portacode 0.3.22__py3-none-any.whl → 0.3.24__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 (25) hide show
  1. portacode/_version.py +16 -3
  2. portacode/connection/handlers/WEBSOCKET_PROTOCOL.md +188 -16
  3. portacode/connection/handlers/__init__.py +4 -0
  4. portacode/connection/handlers/base.py +9 -5
  5. portacode/connection/handlers/chunked_content.py +244 -0
  6. portacode/connection/handlers/file_handlers.py +68 -2
  7. portacode/connection/handlers/project_aware_file_handlers.py +143 -1
  8. portacode/connection/handlers/project_state/git_manager.py +326 -66
  9. portacode/connection/handlers/project_state/handlers.py +307 -31
  10. portacode/connection/handlers/project_state/manager.py +44 -1
  11. portacode/connection/handlers/project_state/models.py +7 -0
  12. portacode/connection/handlers/project_state/utils.py +17 -1
  13. portacode/connection/handlers/project_state_handlers.py +1 -0
  14. portacode/connection/handlers/tab_factory.py +60 -7
  15. portacode/connection/terminal.py +13 -7
  16. {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info}/METADATA +14 -3
  17. {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info}/RECORD +25 -24
  18. {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info}/WHEEL +1 -1
  19. test_modules/test_git_status_ui.py +24 -66
  20. testing_framework/core/playwright_manager.py +23 -0
  21. testing_framework/core/runner.py +10 -2
  22. testing_framework/core/test_discovery.py +7 -3
  23. {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info}/entry_points.txt +0 -0
  24. {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info/licenses}/LICENSE +0 -0
  25. {portacode-0.3.22.dist-info → portacode-0.3.24.dist-info}/top_level.txt +0 -0
@@ -14,6 +14,8 @@ from pathlib import Path
14
14
  from typing import Optional, Dict, Any
15
15
 
16
16
  from .project_state_handlers import TabInfo
17
+ from .project_state.utils import generate_content_hash
18
+ from .file_handlers import cache_content
17
19
 
18
20
  logger = logging.getLogger(__name__)
19
21
 
@@ -57,7 +59,7 @@ MEDIA_EXTENSIONS = {
57
59
  '.ico', '.icns', '.cur', '.psd', '.ai', '.eps', '.raw', '.cr2', '.nef',
58
60
 
59
61
  # Audio
60
- '.mp3', '.wav', '.flac', '.aac', '.ogg', '.wma', '.m4a', '.opus', '.webm',
62
+ '.mp3', '.wav', '.flac', '.aac', '.ogg', '.oga', '.wma', '.m4a', '.opus', '.webm',
61
63
 
62
64
  # Video
63
65
  '.mp4', '.avi', '.mkv', '.mov', '.wmv', '.flv', '.webm', '.m4v', '.3gp',
@@ -140,7 +142,11 @@ class TabFactory:
140
142
  # Determine how to handle the file
141
143
  if extension in IGNORED_EXTENSIONS:
142
144
  tab_info['metadata']['ignored'] = True
143
- tab_info['content'] = f"# Binary file not displayed\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}"
145
+ content = f"# Binary file not displayed\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}"
146
+ tab_info['content'] = content
147
+ content_hash = generate_content_hash(content)
148
+ tab_info['content_hash'] = content_hash
149
+ cache_content(content_hash, content)
144
150
  return TabInfo(**tab_info)
145
151
 
146
152
  # Handle different file types
@@ -181,6 +187,12 @@ class TabFactory:
181
187
  if diff_details:
182
188
  metadata['diff_details'] = diff_details
183
189
 
190
+ # Cache diff content
191
+ original_hash = generate_content_hash(original_content)
192
+ modified_hash = generate_content_hash(modified_content)
193
+ cache_content(original_hash, original_content)
194
+ cache_content(modified_hash, modified_content)
195
+
184
196
  return TabInfo(
185
197
  tab_id=tab_id,
186
198
  tab_type='diff',
@@ -189,6 +201,8 @@ class TabFactory:
189
201
  content=None, # Diff tabs don't use regular content
190
202
  original_content=original_content,
191
203
  modified_content=modified_content,
204
+ original_content_hash=original_hash,
205
+ modified_content_hash=modified_hash,
192
206
  is_dirty=False,
193
207
  mime_type=None,
194
208
  encoding='utf-8',
@@ -219,6 +233,12 @@ class TabFactory:
219
233
  if diff_details:
220
234
  metadata['diff_details'] = diff_details
221
235
 
236
+ # Cache diff content
237
+ original_hash = generate_content_hash(original_content)
238
+ modified_hash = generate_content_hash(modified_content)
239
+ cache_content(original_hash, original_content)
240
+ cache_content(modified_hash, modified_content)
241
+
222
242
  return TabInfo(
223
243
  tab_id=tab_id,
224
244
  tab_type='diff',
@@ -227,6 +247,8 @@ class TabFactory:
227
247
  content=None, # Diff tabs don't use regular content
228
248
  original_content=original_content,
229
249
  modified_content=modified_content,
250
+ original_content_hash=original_hash,
251
+ modified_content_hash=modified_hash,
230
252
  is_dirty=False,
231
253
  mime_type=None,
232
254
  encoding='utf-8',
@@ -248,12 +270,17 @@ class TabFactory:
248
270
  if tab_id is None:
249
271
  tab_id = str(uuid.uuid4())
250
272
 
273
+ # Cache untitled content
274
+ content_hash = generate_content_hash(content)
275
+ cache_content(content_hash, content)
276
+
251
277
  return TabInfo(
252
278
  tab_id=tab_id,
253
279
  tab_type='untitled',
254
280
  title="Untitled",
255
281
  file_path=None,
256
282
  content=content,
283
+ content_hash=content_hash,
257
284
  original_content=None,
258
285
  modified_content=None,
259
286
  is_dirty=bool(content), # Dirty if has initial content
@@ -265,7 +292,11 @@ class TabFactory:
265
292
  async def _load_text_content(self, file_path: Path, tab_info: Dict[str, Any], file_size: int):
266
293
  """Load text content from file."""
267
294
  if file_size > MAX_TEXT_FILE_SIZE:
268
- tab_info['content'] = f"# File too large to display\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}\n# Maximum size for text files: {self._format_file_size(MAX_TEXT_FILE_SIZE)}"
295
+ content = f"# File too large to display\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}\n# Maximum size for text files: {self._format_file_size(MAX_TEXT_FILE_SIZE)}"
296
+ tab_info['content'] = content
297
+ content_hash = generate_content_hash(content)
298
+ tab_info['content_hash'] = content_hash
299
+ cache_content(content_hash, content)
269
300
  tab_info['metadata']['truncated'] = True
270
301
  return
271
302
 
@@ -275,6 +306,9 @@ class TabFactory:
275
306
  try:
276
307
  content = file_path.read_text(encoding=encoding)
277
308
  tab_info['content'] = content
309
+ content_hash = generate_content_hash(content)
310
+ tab_info['content_hash'] = content_hash
311
+ cache_content(content_hash, content)
278
312
  tab_info['encoding'] = encoding
279
313
  self.logger.debug(f"Successfully loaded {file_path} with {encoding} encoding")
280
314
  return
@@ -287,14 +321,22 @@ class TabFactory:
287
321
 
288
322
  except OSError as e:
289
323
  self.logger.error(f"Error reading file {file_path}: {e}")
290
- tab_info['content'] = f"# Error reading file\n# {e}"
324
+ content = f"# Error reading file\n# {e}"
325
+ tab_info['content'] = content
326
+ content_hash = generate_content_hash(content)
327
+ tab_info['content_hash'] = content_hash
328
+ cache_content(content_hash, content)
291
329
  tab_info['metadata']['error'] = str(e)
292
330
 
293
331
  async def _load_media_content(self, file_path: Path, tab_info: Dict[str, Any],
294
332
  file_size: int, mime_type: Optional[str]):
295
333
  """Load media content as base64."""
296
334
  if file_size > MAX_BINARY_FILE_SIZE:
297
- tab_info['content'] = f"# Media file too large to display\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}"
335
+ content = f"# Media file too large to display\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}"
336
+ tab_info['content'] = content
337
+ content_hash = generate_content_hash(content)
338
+ tab_info['content_hash'] = content_hash
339
+ cache_content(content_hash, content)
298
340
  tab_info['metadata']['too_large'] = True
299
341
  return
300
342
 
@@ -313,6 +355,9 @@ class TabFactory:
313
355
  base64_content = base64.b64encode(binary_content).decode('ascii')
314
356
 
315
357
  tab_info['content'] = base64_content
358
+ content_hash = generate_content_hash(base64_content)
359
+ tab_info['content_hash'] = content_hash
360
+ cache_content(content_hash, base64_content)
316
361
  tab_info['encoding'] = 'base64'
317
362
  tab_info['metadata']['original_size'] = file_size
318
363
 
@@ -320,12 +365,20 @@ class TabFactory:
320
365
 
321
366
  except OSError as e:
322
367
  self.logger.error(f"Error reading media file {file_path}: {e}")
323
- tab_info['content'] = f"# Error loading media file\n# {e}"
368
+ content = f"# Error loading media file\n# {e}"
369
+ tab_info['content'] = content
370
+ content_hash = generate_content_hash(content)
371
+ tab_info['content_hash'] = content_hash
372
+ cache_content(content_hash, content)
324
373
  tab_info['metadata']['error'] = str(e)
325
374
 
326
375
  async def _load_binary_content(self, file_path: Path, tab_info: Dict[str, Any], file_size: int):
327
376
  """Handle binary files that can't be displayed."""
328
- tab_info['content'] = f"# Binary file\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}\n# Type: {tab_info.get('mime_type', 'Unknown')}\n\n# This file contains binary data and cannot be displayed as text."
377
+ content = f"# Binary file\n# File: {file_path.name}\n# Size: {self._format_file_size(file_size)}\n# Type: {tab_info.get('mime_type', 'Unknown')}\n\n# This file contains binary data and cannot be displayed as text."
378
+ tab_info['content'] = content
379
+ content_hash = generate_content_hash(content)
380
+ tab_info['content_hash'] = content_hash
381
+ cache_content(content_hash, content)
329
382
  tab_info['metadata']['binary'] = True
330
383
  self.logger.debug(f"Marked {file_path} as binary file")
331
384
 
@@ -30,25 +30,28 @@ from .handlers import (
30
30
  TerminalListHandler,
31
31
  SystemInfoHandler,
32
32
  FileReadHandler,
33
- FileWriteHandler,
34
33
  DirectoryListHandler,
35
34
  FileInfoHandler,
36
35
  FileDeleteHandler,
37
- FileCreateHandler,
38
- FolderCreateHandler,
39
36
  FileRenameHandler,
37
+ ContentRequestHandler,
40
38
  ProjectStateFolderExpandHandler,
41
39
  ProjectStateFolderCollapseHandler,
42
40
  ProjectStateFileOpenHandler,
43
41
  ProjectStateTabCloseHandler,
44
42
  ProjectStateSetActiveTabHandler,
45
43
  ProjectStateDiffOpenHandler,
44
+ ProjectStateDiffContentHandler,
46
45
  ProjectStateGitStageHandler,
47
46
  ProjectStateGitUnstageHandler,
48
47
  ProjectStateGitRevertHandler,
49
48
  ProjectStateGitCommitHandler,
50
49
  )
51
- from .handlers.project_aware_file_handlers import ProjectAwareFileWriteHandler
50
+ from .handlers.project_aware_file_handlers import (
51
+ ProjectAwareFileWriteHandler,
52
+ ProjectAwareFileCreateHandler,
53
+ ProjectAwareFolderCreateHandler,
54
+ )
52
55
  from .handlers.session import SessionManager
53
56
 
54
57
  logger = logging.getLogger(__name__)
@@ -398,6 +401,7 @@ class TerminalManager:
398
401
  "session_manager": self._session_manager,
399
402
  "client_session_manager": self._client_session_manager,
400
403
  "mux": mux,
404
+ "use_content_caching": True, # Enable content caching optimization
401
405
  "debug": self.debug,
402
406
  }
403
407
 
@@ -428,13 +432,14 @@ class TerminalManager:
428
432
  self._command_registry.register(SystemInfoHandler)
429
433
  # File operation handlers
430
434
  self._command_registry.register(FileReadHandler)
431
- self._command_registry.register(FileWriteHandler)
435
+ self._command_registry.register(ProjectAwareFileWriteHandler) # Use project-aware version
432
436
  self._command_registry.register(DirectoryListHandler)
433
437
  self._command_registry.register(FileInfoHandler)
434
438
  self._command_registry.register(FileDeleteHandler)
435
- self._command_registry.register(FileCreateHandler)
436
- self._command_registry.register(FolderCreateHandler)
439
+ self._command_registry.register(ProjectAwareFileCreateHandler) # Use project-aware version
440
+ self._command_registry.register(ProjectAwareFolderCreateHandler) # Use project-aware version
437
441
  self._command_registry.register(FileRenameHandler)
442
+ self._command_registry.register(ContentRequestHandler)
438
443
  # Project state handlers
439
444
  self._command_registry.register(ProjectStateFolderExpandHandler)
440
445
  self._command_registry.register(ProjectStateFolderCollapseHandler)
@@ -442,6 +447,7 @@ class TerminalManager:
442
447
  self._command_registry.register(ProjectStateTabCloseHandler)
443
448
  self._command_registry.register(ProjectStateSetActiveTabHandler)
444
449
  self._command_registry.register(ProjectStateDiffOpenHandler)
450
+ self._command_registry.register(ProjectStateDiffContentHandler)
445
451
  self._command_registry.register(ProjectStateGitStageHandler)
446
452
  self._command_registry.register(ProjectStateGitUnstageHandler)
447
453
  self._command_registry.register(ProjectStateGitRevertHandler)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: portacode
3
- Version: 0.3.22
3
+ Version: 0.3.24
4
4
  Summary: Portacode CLI client and SDK
5
5
  Home-page: https://github.com/portacode/portacode
6
6
  Author: Meena Erian
@@ -18,15 +18,26 @@ Requires-Dist: websockets>=12.0
18
18
  Requires-Dist: pyperclip>=1.8
19
19
  Requires-Dist: psutil>=5.9
20
20
  Requires-Dist: pyte>=0.8
21
+ Requires-Dist: pywinpty>=2.0; platform_system == "Windows"
21
22
  Requires-Dist: GitPython>=3.1.45
22
23
  Requires-Dist: watchdog>=3.0
23
24
  Requires-Dist: diff-match-patch>=20230430
24
25
  Requires-Dist: Pygments>=2.14.0
25
- Requires-Dist: pywinpty>=2.0; platform_system == "Windows"
26
26
  Provides-Extra: dev
27
27
  Requires-Dist: black; extra == "dev"
28
28
  Requires-Dist: flake8; extra == "dev"
29
29
  Requires-Dist: pytest; extra == "dev"
30
+ Dynamic: author
31
+ Dynamic: author-email
32
+ Dynamic: classifier
33
+ Dynamic: description
34
+ Dynamic: description-content-type
35
+ Dynamic: home-page
36
+ Dynamic: license-file
37
+ Dynamic: provides-extra
38
+ Dynamic: requires-dist
39
+ Dynamic: requires-python
40
+ Dynamic: summary
30
41
 
31
42
  # Portacode
32
43
 
@@ -1,7 +1,7 @@
1
1
  portacode/README.md,sha256=4dKtpvR8LNgZPVz37GmkQCMWIr_u25Ao63iW56s7Ke4,775
2
2
  portacode/__init__.py,sha256=oB3sV1wXr-um-RXio73UG8E5Xx6cF2ZVJveqjNmC-vQ,1086
3
3
  portacode/__main__.py,sha256=jmHTGC1hzmo9iKJLv-SSYe9BSIbPPZ2IOpecI03PlTs,296
4
- portacode/_version.py,sha256=n7E4u8v5_JSzz_pY-GEP4KPRYMQlCgu2Xu63pIu5JmA,513
4
+ portacode/_version.py,sha256=-sGbc_e4_IvCzq_UpGJECDvP_BbU_Ba1ROhJvn2pYYU,706
5
5
  portacode/cli.py,sha256=Fcz8aXhgKhoWR9UbtmkN843DVuoJZtCTqBF3K-neVSc,16347
6
6
  portacode/data.py,sha256=5-s291bv8J354myaHm1Y7CQZTZyRzMU3TGe5U4hb-FA,1591
7
7
  portacode/keypair.py,sha256=PAcOYqlVLOoZTPYi6LvLjfsY6BkrWbLOhSZLb8r5sHs,3635
@@ -11,32 +11,34 @@ portacode/connection/README.md,sha256=f9rbuIEKa7cTm9C98rCiBbEtbiIXQU11esGSNhSMiJ
11
11
  portacode/connection/__init__.py,sha256=atqcVGkViIEd7pRa6cP2do07RJOM0UWpbnz5zXjGktU,250
12
12
  portacode/connection/client.py,sha256=tEM4rqzCRxIG5WXqYAT7s65NlEX2Z1sW42GosctBHIA,8013
13
13
  portacode/connection/multiplex.py,sha256=L-TxqJ_ZEbfNEfu1cwxgJ5vUdyRzZjsMy2Kx1diiZys,5237
14
- portacode/connection/terminal.py,sha256=eyzyMaqtDTbQqB8RyB9s66Jnf1jKhpmOrmyvpLbIDng,41304
14
+ portacode/connection/terminal.py,sha256=OfjOjybC2RRrj_a3Eq17qVFF9mD1GlMn7l_O-mIcvIs,41716
15
15
  portacode/connection/handlers/README.md,sha256=HsLZG1QK1JNm67HsgL6WoDg9nxzKXxwkc5fJPFJdX5g,12169
16
- portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=z8kU0ATMl1b_5EgvWqCdRiaPA_0PHfOMvw3_OLpvG04,49881
17
- portacode/connection/handlers/__init__.py,sha256=1SJfeQTkjKoB35izJIYTuPN_4jdo1OHauFAe0WaanHE,2014
18
- portacode/connection/handlers/base.py,sha256=VCu8UO7sf_G3a-YyeE4ZTH8u83ZEcgNZY7Y7-y3Gv1M,6405
19
- portacode/connection/handlers/file_handlers.py,sha256=ZOGUfcaYdXrkUK7RHfwMRNeLdk4z4rQqEeCNkZgaR2g,12981
20
- portacode/connection/handlers/project_aware_file_handlers.py,sha256=cBi43GA7HBhRl4wGbO9I_adMU7jOfUB8lvT2lZJCUoc,3138
21
- portacode/connection/handlers/project_state_handlers.py,sha256=ZySyKn0L_gPKXC5uQf_lRBT33KxOFoOBTfqvXO2RNbM,1650
16
+ portacode/connection/handlers/WEBSOCKET_PROTOCOL.md,sha256=0KBoJzqemXbvpu8Ps7LitLwRYhJNtzcTJ5WAHMGgAuc,62509
17
+ portacode/connection/handlers/__init__.py,sha256=4nv3Z4TGYjWcauKPWsbL_FbrTXApI94V7j6oiU1Vv-o,2144
18
+ portacode/connection/handlers/base.py,sha256=C-H61CUHM2k431CG0usd7eEqklDj9pnuXHujBwhTugk,6666
19
+ portacode/connection/handlers/chunked_content.py,sha256=h6hXRmxSeOgnIxoU8CkmvEf2Odv-ajPrpHIe_W3GKcA,9251
20
+ portacode/connection/handlers/file_handlers.py,sha256=CGMooOrfGbKx-bHA8vr8lmPG-vHw1DJlVWf6TLpfMJU,15355
21
+ portacode/connection/handlers/project_aware_file_handlers.py,sha256=n0M2WmBNWPwzigdIkyZiAsePUQGXVqYSsDyOxm-Nsok,9253
22
+ portacode/connection/handlers/project_state_handlers.py,sha256=v6ZefGW9i7n1aZLq2jOGumJIjYb6aHlPI4m1jkYewm8,1686
22
23
  portacode/connection/handlers/registry.py,sha256=ebi0vhR1XXSYU7mJXlQJ4MjBYaMygGYqX7ReK7vsZ7o,5558
23
24
  portacode/connection/handlers/session.py,sha256=7fHC46YXe5Q3uGxYPGpwcBaU_0eBhH4Hg8fmNVZsVmQ,24528
24
25
  portacode/connection/handlers/system_handlers.py,sha256=65V5ctT0dIBc-oWG91e62MbdvU0z6x6JCTQuIqCWmZ0,5242
25
- portacode/connection/handlers/tab_factory.py,sha256=nKZ-Y3OHwzOU6aG_zLarLKwhJOHeSszPLCLDVuAgQ94,15494
26
+ portacode/connection/handlers/tab_factory.py,sha256=VBZnwtxgeNJCsfBzUjkFWAAGBdijvai4MS2dXnhFY8U,18000
26
27
  portacode/connection/handlers/terminal_handlers.py,sha256=Yuo84zwKB5OiLuVtDLCQgMVrOS3T8ZOONxXpGnnougo,11019
27
28
  portacode/connection/handlers/project_state/README.md,sha256=trdd4ig6ungmwH5SpbSLfyxbL-QgPlGNU-_XrMEiXtw,10114
28
29
  portacode/connection/handlers/project_state/__init__.py,sha256=5ucIqk6Iclqg6bKkL8r_wVs5Tlt6B9J7yQH6yQUt7gc,2541
29
30
  portacode/connection/handlers/project_state/file_system_watcher.py,sha256=w-93ioUZZKZxzPFr8djJnGhWjMVFVdDsmo0fVAukoKk,10150
30
- portacode/connection/handlers/project_state/git_manager.py,sha256=KcLSGGTavDkokI4OWt-cEciIHeTyq4XYhlhq4Av88Aw,64797
31
- portacode/connection/handlers/project_state/handlers.py,sha256=TXekXNxLjNqh1hVo0edyGO7STDRxoZueTrOShKVHXmM,24409
32
- portacode/connection/handlers/project_state/manager.py,sha256=6ZYEP3VD-4YHqdg_SkpVUXKaPExhS18d7Jc6POwjZBk,55645
33
- portacode/connection/handlers/project_state/models.py,sha256=G9oZO3dHf5cRz55GD3l3mkPTU2x1ugMO21uSxp4gxKg,3914
34
- portacode/connection/handlers/project_state/utils.py,sha256=z4zRAhJXM0S0zuiUBc9u8ZZ3qxvCYLO0wHTx-D8YdCk,1289
31
+ portacode/connection/handlers/project_state/git_manager.py,sha256=fA0RbWCblpJep13L4MdqnEP4sE1qWc7Y66vrIo_SWps,76575
32
+ portacode/connection/handlers/project_state/handlers.py,sha256=nkednSbCC-0n3ZtzesaWd9_NFfxNjS4lyVNnbsYs0Zk,37823
33
+ portacode/connection/handlers/project_state/manager.py,sha256=03mN0H9TqVa_ohD5U5-5ZywDGj0s8-y1IxGdb07dZn8,57636
34
+ portacode/connection/handlers/project_state/models.py,sha256=EZTKvxHKs8QlQUbzI0u2IqfzfRRXZixUIDBwTGCJATI,4313
35
+ portacode/connection/handlers/project_state/utils.py,sha256=LsbQr9TH9Bz30FqikmtTxco4PlB_n0kUIuPKQ6Fb_mo,1665
36
+ portacode-0.3.24.dist-info/licenses/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
35
37
  test_modules/README.md,sha256=Do_agkm9WhSzueXjRAkV_xEj6Emy5zB3N3VKY5Roce8,9274
36
38
  test_modules/__init__.py,sha256=1LcbHodIHsB0g-g4NGjSn6AMuCoGbymvXPYLOb6Z7F0,53
37
39
  test_modules/test_device_online.py,sha256=yiSyVaMwKAugqIX_ZIxmLXiOlmA_8IRXiUp12YmpB98,1653
38
40
  test_modules/test_file_operations.py,sha256=KXbh9t8Fah1jZp1pEPlU4_F06iJIJr2fR-yYc4RL6m8,38372
39
- test_modules/test_git_status_ui.py,sha256=nUj2pkbs0aazoYCbXXI7ZCANgVRXVBLrdpbmJjY5OVs,21003
41
+ test_modules/test_git_status_ui.py,sha256=A_qkt-0lFLwxdr7t6YQaM0HqUElDwlZi84mlngg11RA,18734
40
42
  test_modules/test_login_flow.py,sha256=eGjoZQj345qYxNm1arvYVNzjJ-TutT81lSdI6_8A5GU,1685
41
43
  test_modules/test_navigate_testing_folder.py,sha256=-1EXceUEwof_sYp5paMWUNT3mAv5aIpYJ65_vqFbZew,18233
42
44
  test_modules/test_terminal_buffer_performance.py,sha256=YQeDDZVnsQD3ug6udKUZH3NR7PHGP75uZsLZJYya7jg,12183
@@ -52,13 +54,12 @@ testing_framework/core/__init__.py,sha256=8AJQgqSCa9WgwkQNH_wTsA3JmJ4d4FRCweI-io
52
54
  testing_framework/core/base_test.py,sha256=0kKQDNCdAJyTQfJiMBzx9_2MMRrmaVfQF0cawhvian4,13149
53
55
  testing_framework/core/cli_manager.py,sha256=LDH_tWn-CmO08U_rmBIPpN_O6HLaQKRjdnfKGrtqs8Y,6991
54
56
  testing_framework/core/hierarchical_runner.py,sha256=tCeksh2cXbRspurSiE-mQM1M1BOPeY8mKFbjvaBTVHw,26401
55
- testing_framework/core/playwright_manager.py,sha256=z-iw6JN-izHEOxEMkZX0d2s01YE1rCZZONBlgBhoO1Y,18279
56
- testing_framework/core/runner.py,sha256=JzaWgy4xrqfTA72KD51R7pEUvAdL4zS-_4P1HIT9-7I,18880
57
+ testing_framework/core/playwright_manager.py,sha256=9kGXJtRpRNEhaSlV7XVXvx4UQSHSvsFdlSnu74PjrJQ,19296
58
+ testing_framework/core/runner.py,sha256=j2QwNJmAxVBmJvcbVS7DgPJUKPNzqfLmt_4NNdaKmZU,19297
57
59
  testing_framework/core/shared_cli_manager.py,sha256=BESSNtyQb7BOlaOvZmm04T8Uezjms4KCBs2MzTxvzYQ,8790
58
- testing_framework/core/test_discovery.py,sha256=4SxndEINCLwMOw8am8-tWwxeWgW9OBovLMvWziKTsYM,4640
59
- portacode-0.3.22.dist-info/LICENSE,sha256=2FGbCnUDgRYuQTkB1O1dUUpu5CVAjK1j4_p6ack9Z54,1066
60
- portacode-0.3.22.dist-info/METADATA,sha256=6rXExtvIGEolltfdhHSkag1PUx-d4Hej9x2KX23bxVk,6930
61
- portacode-0.3.22.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
62
- portacode-0.3.22.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
63
- portacode-0.3.22.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
64
- portacode-0.3.22.dist-info/RECORD,,
60
+ testing_framework/core/test_discovery.py,sha256=2FZ9fJ8Dp5dloA-fkgXoJ_gCMC_nYPBnA3Hs2xlagzM,4928
61
+ portacode-0.3.24.dist-info/METADATA,sha256=SS67gt2k2J9c4RMudmfgmce73fY93nhqzDHdX7IDZN8,7173
62
+ portacode-0.3.24.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
63
+ portacode-0.3.24.dist-info/entry_points.txt,sha256=lLUUL-BM6_wwe44Xv0__5NQ1BnAz6jWjSMFvZdWW3zU,48
64
+ portacode-0.3.24.dist-info/top_level.txt,sha256=TGhTYUxfW8SyVZc_zGgzjzc24gGT7nSw8Qf73liVRKM,41
65
+ portacode-0.3.24.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.2)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -21,7 +21,7 @@ class GitStatusUITest(BaseTest):
21
21
  description="Test git status expandable section in file explorer UI",
22
22
  tags=["git", "ui", "file-explorer", "expandable"],
23
23
  depends_on=["file_operations_test"],
24
- start_url="/dashboard/"
24
+ start_url="/project/1d98e739-de00-4d65-a13b-c6c82173683f/"
25
25
  )
26
26
 
27
27
 
@@ -31,71 +31,6 @@ class GitStatusUITest(BaseTest):
31
31
  assert_that = self.assert_that()
32
32
  stats = self.stats()
33
33
 
34
- # Navigate to testing_folder project (self-contained navigation)
35
- # The navigate_testing_folder_test dependency ensures the git repo exists,
36
- # but we need to navigate to it ourselves since tests don't share UI state
37
- device_card = page.locator(".device-card.online").filter(has_text="portacode streamer")
38
- await device_card.wait_for()
39
-
40
- editor_button = device_card.get_by_text("Editor")
41
- await editor_button.wait_for()
42
- await editor_button.click()
43
-
44
- # Wait for the project selector modal and select testing_folder
45
- await page.wait_for_selector("#projectSelectorModal.show", timeout=10000)
46
- await self.playwright_manager.take_screenshot("project_modal_opened")
47
-
48
- await page.wait_for_selector(".item-list .section-header", timeout=10000)
49
- await self.playwright_manager.take_screenshot("project_list_loaded")
50
-
51
- # Try to find the testing_folder project specifically
52
- testing_folder_item = page.locator('.item.project').filter(has_text="testing_folder")
53
- testing_folder_count = await testing_folder_item.count()
54
-
55
- # List all available projects for debugging
56
- all_project_items = page.locator('.item.project')
57
- all_project_count = await all_project_items.count()
58
- stats.record_stat("total_projects_available", all_project_count)
59
-
60
- if testing_folder_count > 0:
61
- await testing_folder_item.first.click()
62
- await self.playwright_manager.take_screenshot("testing_folder_selected")
63
- stats.record_stat("found_testing_folder", True)
64
- else:
65
- # Take screenshot of available projects for debugging
66
- await self.playwright_manager.take_screenshot("available_projects_debug")
67
-
68
- # Fallback approach: find any project that might contain git files
69
- if all_project_count > 0:
70
- await all_project_items.first.click()
71
- await self.playwright_manager.take_screenshot("fallback_project_selected")
72
- stats.record_stat("found_testing_folder", False)
73
- stats.record_stat("used_fallback_project", True)
74
- else:
75
- await self.playwright_manager.take_screenshot("no_projects_found")
76
- assert_that.is_true(False, "No projects found in modal - navigate_testing_folder_test may have failed to create the testing_folder")
77
- return TestResult(self.name, False, "No projects available")
78
-
79
- # Wait for modal to close and project to load
80
- await page.wait_for_selector("#projectSelectorModal.show", state="hidden", timeout=10000)
81
- await self.playwright_manager.take_screenshot("project_modal_closed")
82
-
83
- # Wait longer for file explorer to load properly and show project files
84
- await page.wait_for_timeout(8000) # Increased wait time
85
- await self.playwright_manager.take_screenshot("after_longer_wait")
86
-
87
- # Try to wait for specific elements that indicate the project loaded
88
- try:
89
- # Wait for file items to appear (this indicates project loaded successfully)
90
- await page.wait_for_selector(".file-item", timeout=10000)
91
- await self.playwright_manager.take_screenshot("files_appeared")
92
- except:
93
- # If no files appear, continue anyway - maybe it's an empty project
94
- await self.playwright_manager.take_screenshot("no_files_appeared")
95
- pass
96
-
97
- # Take initial screenshot to see the state
98
- await self.playwright_manager.take_screenshot("initial_file_explorer")
99
34
 
100
35
  # Check if project loaded properly - look for files in explorer
101
36
  file_items = page.locator(".file-item")
@@ -121,6 +56,29 @@ class GitStatusUITest(BaseTest):
121
56
 
122
57
  # Continue even if file explorer appears empty - the git section might still work
123
58
  # The title bar shows "testing_folder" which suggests project is loaded
59
+
60
+ # PHASE 0: Test git diff viewer from file explorer
61
+ diff_options = [
62
+ '.context-menu-item:has-text("View Staged Changes")',
63
+ '.context-menu-item:has-text("View Unstaged Changes")',
64
+ '.context-menu-item:has-text("View All Changes")',
65
+ ]
66
+ new_file_item = page.locator('.file-item:has(.file-name:text("new_file1.py"))')
67
+ for option in diff_options:
68
+ await new_file_item.first.click(button='right')
69
+ await page.locator(option).hover()
70
+ await page.wait_for_timeout(500) # Wait for context menu to appear
71
+ await self.playwright_manager.take_screenshot(f"before_clicking_diff_option_{option.split(':')[1]}")
72
+ await page.locator(option).click()
73
+ await page.wait_for_timeout(1000)
74
+
75
+ staged_diff_tab = page.locator('.editor-tab:has-text("(head → staged)")')
76
+ unstaged_diff_tab = page.locator('.editor-tab:has-text("(staged → working)")')
77
+ all_diff_tab = page.locator('.editor-tab:has-text("(head → working)")')
78
+
79
+ assert_that.is_true(await staged_diff_tab.is_visible(), "Staged diff tab should be visible")
80
+ assert_that.is_true(await unstaged_diff_tab.is_visible(), "Unstaged diff tab should be visible")
81
+ assert_that.is_true(await all_diff_tab.is_visible(), "All changes diff tab should be visible")
124
82
 
125
83
  # PHASE 1: Test git status section expansion/collapse
126
84
  stats.start_timer("git_section_detection")
@@ -312,6 +312,29 @@ class PlaywrightManager:
312
312
  except Exception as e:
313
313
  self.logger.error(f"Failed to write actions log: {e}")
314
314
 
315
+ async def log_timeline_marker(self, phase: str, description: str = ""):
316
+ """Log a timeline marker for better test debugging and trace correlation."""
317
+ timestamp = datetime.now().isoformat()
318
+ marker_details = {
319
+ "phase": phase,
320
+ "description": description,
321
+ "timestamp": timestamp
322
+ }
323
+
324
+ # Log to actions for timeline tracking
325
+ await self.log_action("TIMELINE_MARKER", marker_details)
326
+
327
+ # Also log to console for visibility in trace viewer
328
+ if self.page:
329
+ try:
330
+ # Inject a console log into the page that will show up in traces
331
+ script = f"""
332
+ console.log('🧪 TEST PHASE: {phase}' + ({repr(description)} ? ' - ' + {repr(description)} : ''));
333
+ """
334
+ asyncio.create_task(self.page.evaluate(script))
335
+ except Exception as e:
336
+ self.logger.warning(f"Could not inject timeline marker into page: {e}")
337
+
315
338
  def _handle_console_message(self, msg):
316
339
  """Handle console messages from the page."""
317
340
  console_entry = {
@@ -192,11 +192,19 @@ class TestRunner:
192
192
  tb_lines = traceback.format_tb(exc_traceback)
193
193
  user_code_line = None
194
194
 
195
- for line in tb_lines:
196
- if 'test_modules/' in line or 'run(self)' in line:
195
+ # Look for the LAST occurrence in user test code (most specific failure point)
196
+ for line in reversed(tb_lines):
197
+ if 'test_modules/' in line and '.py' in line:
197
198
  user_code_line = line.strip()
198
199
  break
199
200
 
201
+ # If no test_modules line found, look for any line with async context
202
+ if not user_code_line:
203
+ for line in reversed(tb_lines):
204
+ if 'await' in line or 'async' in line:
205
+ user_code_line = line.strip()
206
+ break
207
+
200
208
  # Create detailed error message
201
209
  error_details = [f"Test execution failed: {str(e)}"]
202
210
 
@@ -16,7 +16,7 @@ class TestDiscovery:
16
16
  def __init__(self, test_directories: Optional[List[str]] = None):
17
17
  self.test_directories = test_directories or ["tests", "test_modules"]
18
18
  self.logger = logging.getLogger("test_discovery")
19
- self.logger.setLevel(logging.WARNING) # Only show warnings and errors
19
+ self.logger.setLevel(logging.ERROR) # Show errors during discovery
20
20
  self.discovered_tests: Dict[str, BaseTest] = {}
21
21
 
22
22
  def discover_tests(self, base_path: str = ".") -> Dict[str, BaseTest]:
@@ -60,10 +60,14 @@ class TestDiscovery:
60
60
  self.discovered_tests[test_instance.name] = test_instance
61
61
  self.logger.debug(f"Discovered test: {test_instance.name}")
62
62
  except Exception as e:
63
- self.logger.error(f"Failed to instantiate test {name}: {e}")
63
+ error_msg = f"Failed to instantiate test {name} in {file_path}: {e}"
64
+ self.logger.error(error_msg)
65
+ print(f"❌ {error_msg}") # Also print to console for immediate visibility
64
66
 
65
67
  except Exception as e:
66
- self.logger.error(f"Failed to load test file {file_path}: {e}")
68
+ error_msg = f"Failed to load test file {file_path}: {e}"
69
+ self.logger.error(error_msg)
70
+ print(f"❌ {error_msg}") # Also print to console for immediate visibility
67
71
 
68
72
  def get_tests_by_category(self, category: TestCategory) -> List[BaseTest]:
69
73
  """Get all tests belonging to a specific category."""