vibe-reader 0.2.0__tar.gz → 0.2.2__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 (194) hide show
  1. vibe_reader-0.2.2/.github/dependabot.yml +18 -0
  2. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/.github/workflows/publish.yml +4 -4
  3. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/PKG-INFO +9 -9
  4. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/backend/files.py +33 -3
  5. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/backend/rendering.py +10 -1
  6. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/api.js +9 -4
  7. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/files-view.js +43 -1
  8. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/state.js +3 -1
  9. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/pyproject.toml +8 -8
  10. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/tests/test_files.py +58 -0
  11. vibe_reader-0.2.2/tests/test_frontend_modules.py +185 -0
  12. vibe_reader-0.2.2/uv.lock +413 -0
  13. vibe_reader-0.2.0/tests/test_frontend_modules.py +0 -65
  14. vibe_reader-0.2.0/uv.lock +0 -389
  15. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/.gitignore +0 -0
  16. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/AGENTS.md +0 -0
  17. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/README.md +0 -0
  18. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/backend/__init__.py +0 -0
  19. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/backend/git.py +0 -0
  20. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/backend/main.py +0 -0
  21. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/backend/services/__init__.py +0 -0
  22. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/backend/services/git_diff_service.py +0 -0
  23. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/backend/services/path_service.py +0 -0
  24. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/backend/services/tmux_service.py +0 -0
  25. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/backend/tmux.py +0 -0
  26. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/doc/DESIGN.md +0 -0
  27. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/app.js +0 -0
  28. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/config.js +0 -0
  29. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/content-renderer.js +0 -0
  30. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/favicon.png +0 -0
  31. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/favicon.svg +0 -0
  32. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/file-status.js +0 -0
  33. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/index.html +0 -0
  34. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/manifest.json +0 -0
  35. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/mermaid-renderer.js +0 -0
  36. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/styles.css +0 -0
  37. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/sw.js +0 -0
  38. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/tmux-refresh.js +0 -0
  39. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/tmux-tree.js +0 -0
  40. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/tmux-view.js +0 -0
  41. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/ui.js +0 -0
  42. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/architecture-PBZL5I3N-X3DJOG6M.mjs +0 -0
  43. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/architecture-PBZL5I3N-X3DJOG6M.mjs.map +0 -0
  44. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/architectureDiagram-EJXTDGMB.mjs +0 -0
  45. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/architectureDiagram-EJXTDGMB.mjs.map +0 -0
  46. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/blockDiagram-HG7WUIX4.mjs +0 -0
  47. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/blockDiagram-HG7WUIX4.mjs.map +0 -0
  48. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/c4Diagram-BBK6TRR6.mjs +0 -0
  49. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/c4Diagram-BBK6TRR6.mjs.map +0 -0
  50. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-33NP3AWU.mjs +0 -0
  51. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-33NP3AWU.mjs.map +0 -0
  52. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-3UWU4A3N.mjs +0 -0
  53. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-3UWU4A3N.mjs.map +0 -0
  54. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-3YCYZ6SJ.mjs +0 -0
  55. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-3YCYZ6SJ.mjs.map +0 -0
  56. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-3ZBQQEZ6.mjs +0 -0
  57. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-3ZBQQEZ6.mjs.map +0 -0
  58. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-5YHUCXBM.mjs +0 -0
  59. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-5YHUCXBM.mjs.map +0 -0
  60. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-7RZVMHOQ.mjs +0 -0
  61. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-7RZVMHOQ.mjs.map +0 -0
  62. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-AEOMTBSW.mjs +0 -0
  63. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-AEOMTBSW.mjs.map +0 -0
  64. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-AHTCUU5T.mjs +0 -0
  65. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-AHTCUU5T.mjs.map +0 -0
  66. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-BDKIFH7H.mjs +0 -0
  67. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-BDKIFH7H.mjs.map +0 -0
  68. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-D2KP3OBD.mjs +0 -0
  69. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-D2KP3OBD.mjs.map +0 -0
  70. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-D4HHKWPY.mjs +0 -0
  71. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-D4HHKWPY.mjs.map +0 -0
  72. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-F3E5R7LK.mjs +0 -0
  73. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-F3E5R7LK.mjs.map +0 -0
  74. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-GAX3EE6F.mjs +0 -0
  75. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-GAX3EE6F.mjs.map +0 -0
  76. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-H3VCZNTA.mjs +0 -0
  77. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-H3VCZNTA.mjs.map +0 -0
  78. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-HN6EAY2L.mjs +0 -0
  79. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-HN6EAY2L.mjs.map +0 -0
  80. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-IWDTEBJL.mjs +0 -0
  81. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-IWDTEBJL.mjs.map +0 -0
  82. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-JIN56HTB.mjs +0 -0
  83. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-JIN56HTB.mjs.map +0 -0
  84. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-JVPIJFVP.mjs +0 -0
  85. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-JVPIJFVP.mjs.map +0 -0
  86. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-KSICW3F5.mjs +0 -0
  87. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-KSICW3F5.mjs.map +0 -0
  88. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-MFSPT3DZ.mjs +0 -0
  89. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-MFSPT3DZ.mjs.map +0 -0
  90. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-MGPAVIPZ.mjs +0 -0
  91. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-MGPAVIPZ.mjs.map +0 -0
  92. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-NHJX6F6M.mjs +0 -0
  93. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-NHJX6F6M.mjs.map +0 -0
  94. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-NN2NZ22Q.mjs +0 -0
  95. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-NN2NZ22Q.mjs.map +0 -0
  96. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-Q3CC2MQB.mjs +0 -0
  97. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-Q3CC2MQB.mjs.map +0 -0
  98. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-QU3B7NT4.mjs +0 -0
  99. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-QU3B7NT4.mjs.map +0 -0
  100. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-SK62O5VA.mjs +0 -0
  101. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-SK62O5VA.mjs.map +0 -0
  102. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-SYGLAQYM.mjs +0 -0
  103. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-SYGLAQYM.mjs.map +0 -0
  104. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-T4EQAHMB.mjs +0 -0
  105. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-T4EQAHMB.mjs.map +0 -0
  106. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-TBF5ZNIQ.mjs +0 -0
  107. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-TBF5ZNIQ.mjs.map +0 -0
  108. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-TFLKLN34.mjs +0 -0
  109. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-TFLKLN34.mjs.map +0 -0
  110. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-TJ4WWVTA.mjs +0 -0
  111. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-TJ4WWVTA.mjs.map +0 -0
  112. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-TU3PZOEN.mjs +0 -0
  113. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-TU3PZOEN.mjs.map +0 -0
  114. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-VELTKBKT.mjs +0 -0
  115. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-VELTKBKT.mjs.map +0 -0
  116. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-W2A4CRWB.mjs +0 -0
  117. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-W2A4CRWB.mjs.map +0 -0
  118. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-XBXGYYE5.mjs +0 -0
  119. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-XBXGYYE5.mjs.map +0 -0
  120. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-XODN6PIJ.mjs +0 -0
  121. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-XODN6PIJ.mjs.map +0 -0
  122. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-YLHEXJF3.mjs +0 -0
  123. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-YLHEXJF3.mjs.map +0 -0
  124. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-ZX4KS3OZ.mjs +0 -0
  125. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/chunk-ZX4KS3OZ.mjs.map +0 -0
  126. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/classDiagram-JG6F6YBT.mjs +0 -0
  127. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/classDiagram-JG6F6YBT.mjs.map +0 -0
  128. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/classDiagram-v2-CFK7UN3Z.mjs +0 -0
  129. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/classDiagram-v2-CFK7UN3Z.mjs.map +0 -0
  130. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/cose-bilkent-PNC4W37J.mjs +0 -0
  131. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/cose-bilkent-PNC4W37J.mjs.map +0 -0
  132. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/dagre-GNUAUHNH.mjs +0 -0
  133. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/dagre-GNUAUHNH.mjs.map +0 -0
  134. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/diagram-G7E7SVAT.mjs +0 -0
  135. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/diagram-G7E7SVAT.mjs.map +0 -0
  136. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/diagram-HOHITQNR.mjs +0 -0
  137. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/diagram-HOHITQNR.mjs.map +0 -0
  138. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/diagram-SUXTHJMA.mjs +0 -0
  139. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/diagram-SUXTHJMA.mjs.map +0 -0
  140. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/erDiagram-L2NYR2MQ.mjs +0 -0
  141. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/erDiagram-L2NYR2MQ.mjs.map +0 -0
  142. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/flowDiagram-IIOBCMXN.mjs +0 -0
  143. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/flowDiagram-IIOBCMXN.mjs.map +0 -0
  144. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/ganttDiagram-CBGYKTO2.mjs +0 -0
  145. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/ganttDiagram-CBGYKTO2.mjs.map +0 -0
  146. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/gitGraph-HDMCJU4V-C6Y2HLJ7.mjs +0 -0
  147. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/gitGraph-HDMCJU4V-C6Y2HLJ7.mjs.map +0 -0
  148. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/gitGraphDiagram-FCDOK2W6.mjs +0 -0
  149. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/gitGraphDiagram-FCDOK2W6.mjs.map +0 -0
  150. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/info-3K5VOQVL-GYCQBKJT.mjs +0 -0
  151. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/info-3K5VOQVL-GYCQBKJT.mjs.map +0 -0
  152. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/infoDiagram-JC2SMEGH.mjs +0 -0
  153. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/infoDiagram-JC2SMEGH.mjs.map +0 -0
  154. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/ishikawaDiagram-LCPTV24A.mjs +0 -0
  155. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/ishikawaDiagram-LCPTV24A.mjs.map +0 -0
  156. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/journeyDiagram-FZRSWBTQ.mjs +0 -0
  157. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/journeyDiagram-FZRSWBTQ.mjs.map +0 -0
  158. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/kanban-definition-ETIEC4YN.mjs +0 -0
  159. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/kanban-definition-ETIEC4YN.mjs.map +0 -0
  160. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/katex-GD7MH7QM.mjs +0 -0
  161. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/katex-GD7MH7QM.mjs.map +0 -0
  162. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/mindmap-definition-QTQOGDLH.mjs +0 -0
  163. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/mindmap-definition-QTQOGDLH.mjs.map +0 -0
  164. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/packet-RMMSAZCW-74OXTHQQ.mjs +0 -0
  165. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/packet-RMMSAZCW-74OXTHQQ.mjs.map +0 -0
  166. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/pie-UPGHQEXC-SNR2CZVS.mjs +0 -0
  167. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/pie-UPGHQEXC-SNR2CZVS.mjs.map +0 -0
  168. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/pieDiagram-O4VNXU6G.mjs +0 -0
  169. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/pieDiagram-O4VNXU6G.mjs.map +0 -0
  170. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/quadrantDiagram-DYU4IJOS.mjs +0 -0
  171. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/quadrantDiagram-DYU4IJOS.mjs.map +0 -0
  172. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/radar-KQ55EAFF-DTKK6RDY.mjs +0 -0
  173. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/radar-KQ55EAFF-DTKK6RDY.mjs.map +0 -0
  174. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/requirementDiagram-N2XL27SN.mjs +0 -0
  175. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/requirementDiagram-N2XL27SN.mjs.map +0 -0
  176. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/sankeyDiagram-HWP7ZNIN.mjs +0 -0
  177. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/sankeyDiagram-HWP7ZNIN.mjs.map +0 -0
  178. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/sequenceDiagram-PYXOKC54.mjs +0 -0
  179. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/sequenceDiagram-PYXOKC54.mjs.map +0 -0
  180. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/stateDiagram-UFOXNHOX.mjs +0 -0
  181. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/stateDiagram-UFOXNHOX.mjs.map +0 -0
  182. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/stateDiagram-v2-C774GKM7.mjs +0 -0
  183. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/stateDiagram-v2-C774GKM7.mjs.map +0 -0
  184. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/timeline-definition-GC5UGDFR.mjs +0 -0
  185. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/timeline-definition-GC5UGDFR.mjs.map +0 -0
  186. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/treemap-KZPCXAKY-RU5UWGQG.mjs +0 -0
  187. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/treemap-KZPCXAKY-RU5UWGQG.mjs.map +0 -0
  188. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/vennDiagram-R4MEU4WM.mjs +0 -0
  189. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/vennDiagram-R4MEU4WM.mjs.map +0 -0
  190. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/xychartDiagram-TMDUDL2A.mjs +0 -0
  191. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/chunks/mermaid.esm.min/xychartDiagram-TMDUDL2A.mjs.map +0 -0
  192. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/frontend/vendor/mermaid.esm.min.mjs +0 -0
  193. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/tests/test_git_diff.py +0 -0
  194. {vibe_reader-0.2.0 → vibe_reader-0.2.2}/tests/test_tmux.py +0 -0
@@ -0,0 +1,18 @@
1
+ version: 2
2
+
3
+ updates:
4
+ - package-ecosystem: "uv"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ day: "monday"
9
+ time: "09:00"
10
+ timezone: "Etc/UTC"
11
+
12
+ - package-ecosystem: "github-actions"
13
+ directory: "/"
14
+ schedule:
15
+ interval: "weekly"
16
+ day: "monday"
17
+ time: "09:00"
18
+ timezone: "Etc/UTC"
@@ -13,18 +13,18 @@ jobs:
13
13
  contents: read
14
14
 
15
15
  steps:
16
- - uses: actions/checkout@v6.0.3
16
+ - uses: actions/checkout@v7.0.1
17
17
 
18
18
  - name: Set up Python
19
- uses: actions/setup-python@v6.2.0
19
+ uses: actions/setup-python@v7.0.0
20
20
  with:
21
21
  python-version: '3.13'
22
22
 
23
23
  - name: Install uv
24
- uses: astral-sh/setup-uv@v8.2.0
24
+ uses: astral-sh/setup-uv@v10.2.0
25
25
 
26
26
  - name: Build package
27
27
  run: uv build
28
28
 
29
29
  - name: Publish to PyPI
30
- uses: pypa/gh-action-pypi-publish@v1.14.0
30
+ uses: pypa/gh-action-pypi-publish@v1.14.2
@@ -1,19 +1,19 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: vibe-reader
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: E-ink friendly web reader for tmux panes, project files, Markdown, and git diffs.
5
5
  Author-email: PsychArch <PsychArch@github.com>
6
6
  Requires-Python: >=3.13
7
- Requires-Dist: dulwich==1.2.6
8
- Requires-Dist: fastapi==0.136.3
9
- Requires-Dist: libtmux==0.58.0
10
- Requires-Dist: markdown==3.10.2
11
- Requires-Dist: pydantic==2.13.4
7
+ Requires-Dist: dulwich==1.2.15
8
+ Requires-Dist: fastapi==0.141.1
9
+ Requires-Dist: libtmux==0.62.0
10
+ Requires-Dist: markdown==3.11
11
+ Requires-Dist: pydantic==2.13.5
12
12
  Requires-Dist: pygments==2.20.0
13
- Requires-Dist: uvicorn==0.49.0
13
+ Requires-Dist: uvicorn==0.54.0
14
14
  Provides-Extra: dev
15
15
  Requires-Dist: httpx2>=2.4.0; extra == 'dev'
16
- Requires-Dist: pytest==9.0.3; extra == 'dev'
16
+ Requires-Dist: pytest==9.1.1; extra == 'dev'
17
17
  Description-Content-Type: text/markdown
18
18
 
19
19
  # Vibe Reader
@@ -3,6 +3,7 @@ from pathlib import Path
3
3
  from typing import AsyncIterator
4
4
 
5
5
  from fastapi import APIRouter, FastAPI, HTTPException
6
+ from starlette.concurrency import run_in_threadpool
6
7
 
7
8
  from backend.rendering import render_text_content
8
9
  from backend.services.path_service import (
@@ -15,6 +16,9 @@ from backend.services.path_service import (
15
16
 
16
17
  # Maximum file size to read (10MB)
17
18
  MAX_FILE_SIZE = 10 * 1024 * 1024
19
+ MAX_HIGHLIGHT_FILE_SIZE = 512 * 1024
20
+ MAX_MARKDOWN_RENDER_FILE_SIZE = 1024 * 1024
21
+ LARGE_FILE_RENDER_NOTE = "Large file rendered without syntax highlighting."
18
22
 
19
23
 
20
24
  def get_project_root() -> Path:
@@ -37,6 +41,28 @@ async def files_lifespan(_: FastAPI) -> AsyncIterator[None]:
37
41
 
38
42
  router = APIRouter(prefix="/api/files", tags=["files"], lifespan=files_lifespan)
39
43
 
44
+
45
+ def _should_force_plain_render(path: Path, file_size: int, highlight: bool) -> bool:
46
+ if path.suffix.lower() in {".md", ".markdown"}:
47
+ return file_size > MAX_MARKDOWN_RENDER_FILE_SIZE
48
+ return highlight and file_size > MAX_HIGHLIGHT_FILE_SIZE
49
+
50
+
51
+ def _read_and_render_file(target_path: Path, file_size: int, highlight: bool):
52
+ force_plain = _should_force_plain_render(target_path, file_size, highlight)
53
+ content = target_path.read_text()
54
+ render_result = render_text_content(
55
+ target_path,
56
+ content,
57
+ enable_highlighting=highlight,
58
+ force_plain=force_plain,
59
+ )
60
+ metadata = dict(render_result.metadata)
61
+ if force_plain:
62
+ metadata["large_file"] = "true"
63
+ metadata["render_note"] = LARGE_FILE_RENDER_NOTE
64
+ return render_result, metadata
65
+
40
66
  @router.get("")
41
67
  async def list_files(path: str = "."):
42
68
  """List files in a directory"""
@@ -67,14 +93,18 @@ async def get_file_content(path: str, highlight: bool = True):
67
93
  if file_size > MAX_FILE_SIZE:
68
94
  raise HTTPException(status_code=400, detail=f"File too large (max {MAX_FILE_SIZE // 1024 // 1024}MB)")
69
95
 
70
- content = target_path.read_text()
71
- render_result = render_text_content(target_path, content, enable_highlighting=highlight)
96
+ render_result, metadata = await run_in_threadpool(
97
+ _read_and_render_file,
98
+ target_path,
99
+ file_size,
100
+ highlight,
101
+ )
72
102
 
73
103
  return {
74
104
  "path": display_path(project_root, target_path),
75
105
  "render_mode": render_result.mode,
76
106
  "html": render_result.html,
77
- "metadata": render_result.metadata,
107
+ "metadata": metadata,
78
108
  }
79
109
  except UnicodeDecodeError:
80
110
  raise HTTPException(status_code=400, detail="Cannot read binary file")
@@ -28,9 +28,18 @@ class RenderResult:
28
28
  metadata: Dict[str, str] = field(default_factory=dict)
29
29
 
30
30
 
31
- def render_text_content(path: Path, content: str, enable_highlighting: bool = True) -> RenderResult:
31
+ def render_text_content(
32
+ path: Path,
33
+ content: str,
34
+ enable_highlighting: bool = True,
35
+ *,
36
+ force_plain: bool = False,
37
+ ) -> RenderResult:
32
38
  """Render text content based on file type."""
33
39
 
40
+ if force_plain:
41
+ return _render_plain(content)
42
+
34
43
  suffix = path.suffix.lower()
35
44
  if suffix in _MARKDOWN_EXTENSIONS:
36
45
  return _render_markdown(content, enable_highlighting=enable_highlighting)
@@ -1,8 +1,12 @@
1
- export async function fetchJSON(url, errorContext) {
1
+ export async function fetchJSON(url, errorContext, options = {}) {
2
2
  let response;
3
3
  try {
4
- response = await fetch(url);
4
+ response = await fetch(url, options);
5
5
  } catch (networkError) {
6
+ if (networkError.name === 'AbortError') {
7
+ throw networkError;
8
+ }
9
+
6
10
  const error = new Error(`${errorContext}: ${networkError.message}`);
7
11
  error.context = errorContext;
8
12
  error.url = url;
@@ -61,10 +65,11 @@ export function loadFiles(path) {
61
65
  return fetchJSON(`/api/files?path=${encodeURIComponent(path)}`, 'load files');
62
66
  }
63
67
 
64
- export function loadFileContent(path, { enableHighlighting }) {
68
+ export function loadFileContent(path, { enableHighlighting, signal } = {}) {
65
69
  return fetchJSON(
66
70
  `/api/files/content?path=${encodeURIComponent(path)}&highlight=${enableHighlighting}`,
67
- 'load file'
71
+ 'load file',
72
+ { signal }
68
73
  );
69
74
  }
70
75
 
@@ -48,6 +48,20 @@ export function createFilesViewController({ dom, state, viewState, api, contentR
48
48
  return source === 'staged' ? 'Staged' : 'Unstaged';
49
49
  }
50
50
 
51
+ function abortPendingFileContentRequest() {
52
+ if (!state.currentFileAbortController) {
53
+ return;
54
+ }
55
+
56
+ state.currentFileAbortController.abort();
57
+ state.currentFileAbortController = null;
58
+ }
59
+
60
+ function invalidateFileContentRequests() {
61
+ abortPendingFileContentRequest();
62
+ state.currentFileRequestId += 1;
63
+ }
64
+
51
65
  function openFile(path, { isDeleted = false } = {}) {
52
66
  if (!path) {
53
67
  return;
@@ -58,6 +72,7 @@ export function createFilesViewController({ dom, state, viewState, api, contentR
58
72
  return;
59
73
  }
60
74
  if (isDeleted) {
75
+ invalidateFileContentRequests();
61
76
  callbacks.showToast('Enable diff mode to inspect deleted files.');
62
77
  contentRenderer.renderDiffPrompt();
63
78
  return;
@@ -66,6 +81,8 @@ export function createFilesViewController({ dom, state, viewState, api, contentR
66
81
  }
67
82
 
68
83
  async function loadGitDiff(path, source = state.diffSource) {
84
+ invalidateFileContentRequests();
85
+
69
86
  if (!path) {
70
87
  contentRenderer.renderDiffPrompt();
71
88
  return;
@@ -165,6 +182,7 @@ export function createFilesViewController({ dom, state, viewState, api, contentR
165
182
  setCurrentDirectory(parentPath);
166
183
  state.selectedFile = null;
167
184
  state.selectedFileIsDeleted = false;
185
+ invalidateFileContentRequests();
168
186
  contentRenderer.resetFilePreview();
169
187
  loadFileList({ forceStatusRefresh: true });
170
188
  }
@@ -187,6 +205,7 @@ export function createFilesViewController({ dom, state, viewState, api, contentR
187
205
  setCurrentDirectory(file.path);
188
206
  state.selectedFile = null;
189
207
  state.selectedFileIsDeleted = false;
208
+ invalidateFileContentRequests();
190
209
  contentRenderer.resetFilePreview();
191
210
  loadFileList();
192
211
  } else {
@@ -241,15 +260,38 @@ export function createFilesViewController({ dom, state, viewState, api, contentR
241
260
  }
242
261
 
243
262
  async function loadFileContent(path) {
263
+ abortPendingFileContentRequest();
264
+
265
+ const requestId = ++state.currentFileRequestId;
266
+ const abortController = new AbortController();
267
+ state.currentFileAbortController = abortController;
268
+ contentRenderer.renderInlineMessage(`Loading ${path}...`);
269
+
244
270
  try {
245
271
  const config = readConfig();
246
272
  const data = await api.loadFileContent(path, {
247
- enableHighlighting: config.enableSyntaxHighlighting
273
+ enableHighlighting: config.enableSyntaxHighlighting,
274
+ signal: abortController.signal
248
275
  });
276
+
277
+ if (requestId !== state.currentFileRequestId) {
278
+ return;
279
+ }
280
+
249
281
  contentRenderer.renderFileContent(data);
250
282
  dom.fileContentContainer.scrollTop = 0;
283
+ if (data.metadata?.render_note) {
284
+ callbacks.showToast(data.metadata.render_note);
285
+ }
251
286
  } catch (error) {
287
+ if (requestId !== state.currentFileRequestId || error.name === 'AbortError') {
288
+ return;
289
+ }
252
290
  contentRenderer.renderInlineMessage(error.message);
291
+ } finally {
292
+ if (requestId === state.currentFileRequestId) {
293
+ state.currentFileAbortController = null;
294
+ }
253
295
  }
254
296
  }
255
297
 
@@ -26,7 +26,9 @@ export function createFilesState() {
26
26
  diffSource: 'unstaged',
27
27
  currentDiffRequestId: 0,
28
28
  selectedFileIsDeleted: false,
29
- currentFileRenderToken: 0
29
+ currentFileRenderToken: 0,
30
+ currentFileRequestId: 0,
31
+ currentFileAbortController: null
30
32
  };
31
33
  }
32
34
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "vibe-reader"
3
- version = "0.2.0"
3
+ version = "0.2.2"
4
4
  description = "E-ink friendly web reader for tmux panes, project files, Markdown, and git diffs."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -8,13 +8,13 @@ authors = [
8
8
  ]
9
9
  requires-python = ">=3.13"
10
10
  dependencies = [
11
- "fastapi==0.136.3",
12
- "libtmux==0.58.0",
13
- "uvicorn==0.49.0",
11
+ "fastapi==0.141.1",
12
+ "libtmux==0.62.0",
13
+ "uvicorn==0.54.0",
14
14
  "Pygments==2.20.0",
15
- "markdown==3.10.2",
16
- "dulwich==1.2.6",
17
- "pydantic==2.13.4",
15
+ "markdown==3.11",
16
+ "dulwich==1.2.15",
17
+ "pydantic==2.13.5",
18
18
  ]
19
19
 
20
20
  [project.scripts]
@@ -22,7 +22,7 @@ vibe-reader = "backend.main:main"
22
22
 
23
23
  [project.optional-dependencies]
24
24
  dev = [
25
- "pytest==9.0.3",
25
+ "pytest==9.1.1",
26
26
  "httpx2>=2.4.0",
27
27
  ]
28
28
 
@@ -154,6 +154,64 @@ def test_plain_mode_when_no_lexer(tmp_path):
154
154
  assert "line-code" in payload["html"]
155
155
 
156
156
 
157
+ def test_large_code_file_skips_syntax_highlighting(tmp_path):
158
+ project_root = tmp_path / "proj"
159
+ project_root.mkdir()
160
+ code_file = project_root / "large.py"
161
+ code_file.write_text("def hello():\n return 'world'\n" * 20000, encoding="utf-8")
162
+
163
+ with client_with_root(project_root) as client:
164
+ response = client.get(
165
+ "/api/files/content",
166
+ params={"path": "large.py"},
167
+ )
168
+
169
+ payload = response.json()
170
+
171
+ assert response.status_code == 200
172
+ assert payload["render_mode"] == "plain"
173
+ assert "tok-" not in payload["html"]
174
+ assert payload["metadata"]["large_file"] == "true"
175
+ assert payload["metadata"]["render_note"] == "Large file rendered without syntax highlighting."
176
+
177
+
178
+ def test_large_markdown_file_skips_markdown_rendering(tmp_path):
179
+ project_root = tmp_path / "proj"
180
+ project_root.mkdir()
181
+ markdown_file = project_root / "large.md"
182
+ markdown_file.write_text("# Heading\n\n```python\nprint('ok')\n```\n\n" * 35000, encoding="utf-8")
183
+
184
+ with client_with_root(project_root) as client:
185
+ response = client.get(
186
+ "/api/files/content",
187
+ params={"path": "large.md"},
188
+ )
189
+
190
+ payload = response.json()
191
+
192
+ assert response.status_code == 200
193
+ assert payload["render_mode"] == "plain"
194
+ assert 'class="md-content"' not in payload["html"]
195
+ assert "tok-" not in payload["html"]
196
+ assert payload["metadata"]["large_file"] == "true"
197
+
198
+
199
+ def test_file_size_limit_still_rejects_oversized_file(tmp_path):
200
+ project_root = tmp_path / "proj"
201
+ project_root.mkdir()
202
+ large_file = project_root / "too-large.txt"
203
+ large_file.write_bytes(b"x" * (10 * 1024 * 1024 + 1))
204
+
205
+ with client_with_root(project_root) as client:
206
+ response = client.get(
207
+ "/api/files/content",
208
+ params={"path": "too-large.txt"},
209
+ )
210
+
211
+ assert response.status_code == 400
212
+ assert response.json()["detail"] == "File too large (max 10MB)"
213
+
214
+
157
215
  def test_list_files_respects_configured_root(tmp_path):
158
216
  project_root = tmp_path / "proj"
159
217
  project_root.mkdir()
@@ -0,0 +1,185 @@
1
+ import shutil
2
+ import subprocess
3
+ from pathlib import Path
4
+
5
+ import pytest
6
+
7
+
8
+ PROJECT_ROOT = Path(__file__).resolve().parents[1]
9
+
10
+
11
+ def test_frontend_state_and_tmux_tree_helpers():
12
+ node = shutil.which("node")
13
+ if node is None:
14
+ pytest.skip("node is not available")
15
+
16
+ script = r"""
17
+ import assert from 'node:assert/strict';
18
+ import {
19
+ createConfigState,
20
+ createFilesState,
21
+ createMermaidState,
22
+ createTmuxState,
23
+ createViewState
24
+ } from './frontend/state.js';
25
+ import { createFilesViewController } from './frontend/files-view.js';
26
+ import {
27
+ findFirstSelectablePane,
28
+ getSelectedPaneKey
29
+ } from './frontend/tmux-tree.js';
30
+
31
+ function createDeferred() {
32
+ let resolve;
33
+ let reject;
34
+ const promise = new Promise((promiseResolve, promiseReject) => {
35
+ resolve = promiseResolve;
36
+ reject = promiseReject;
37
+ });
38
+ return { promise, resolve, reject };
39
+ }
40
+
41
+ function createFileControllerHarness(apiOverrides = {}) {
42
+ const fileState = createFilesState();
43
+ const calls = {
44
+ inlineMessages: [],
45
+ renderedPayloads: [],
46
+ toasts: []
47
+ };
48
+ const dom = {
49
+ fileContentContainer: { scrollTop: 45 },
50
+ fileList: { querySelectorAll: () => [] },
51
+ pageUpFiles: { addEventListener: () => {} },
52
+ pageDownFiles: { addEventListener: () => {} },
53
+ loadFiles: { addEventListener: () => {} },
54
+ diffModeToggle: { addEventListener: () => {} },
55
+ currentPathDisplay: null
56
+ };
57
+ const contentRenderer = {
58
+ renderInlineMessage: (message) => calls.inlineMessages.push(message),
59
+ renderFileContent: (payload) => calls.renderedPayloads.push(payload),
60
+ renderDiffPrompt: (message) => calls.inlineMessages.push(message || 'diff prompt'),
61
+ resetFilePreview: () => calls.inlineMessages.push('reset')
62
+ };
63
+ const controller = createFilesViewController({
64
+ dom,
65
+ state: fileState,
66
+ viewState: createViewState(),
67
+ api: {
68
+ loadFileContent: () => Promise.resolve({ render_mode: 'plain', html: '', metadata: {} }),
69
+ loadFiles: () => Promise.resolve([]),
70
+ loadGitDiff: () => Promise.resolve({ text: '' }),
71
+ ...apiOverrides
72
+ },
73
+ contentRenderer,
74
+ gitStatus: {
75
+ getSummary: () => Promise.resolve(null),
76
+ invalidate: () => {}
77
+ },
78
+ callbacks: {
79
+ markInteraction: () => {},
80
+ showToast: (message) => calls.toasts.push(message)
81
+ }
82
+ });
83
+ return { calls, controller, dom, fileState };
84
+ }
85
+
86
+ assert.deepEqual(createViewState(), {
87
+ currentView: 'tmux',
88
+ sidebarCollapsed: false
89
+ });
90
+ assert.equal(createTmuxState().autoRefreshEnabled, false);
91
+ assert.equal(createFilesState().diffSource, 'unstaged');
92
+ assert.equal(createFilesState().currentFileRequestId, 0);
93
+ assert.equal(createFilesState().currentFileAbortController, null);
94
+ assert.equal(createConfigState().initialized, false);
95
+ assert.equal(createMermaidState().diagramId, 0);
96
+
97
+ assert.equal(getSelectedPaneKey(null), null);
98
+ assert.equal(
99
+ getSelectedPaneKey({ session: 'work', window: 1, pane: 2 }),
100
+ 'work\u00001\u00002'
101
+ );
102
+ assert.deepEqual(
103
+ findFirstSelectablePane([
104
+ { name: 'empty', windows: [] },
105
+ {
106
+ name: 'dev',
107
+ windows: [
108
+ { index: '0', panes: [] },
109
+ { index: '1', panes: [{ index: '3' }] }
110
+ ]
111
+ }
112
+ ]),
113
+ { session: 'dev', window: '1', pane: '3' }
114
+ );
115
+ assert.equal(findFirstSelectablePane([{ name: 'empty', windows: [] }]), null);
116
+
117
+ {
118
+ const request = createDeferred();
119
+ let capturedSignal = null;
120
+ const { calls, controller, dom } = createFileControllerHarness({
121
+ loadFileContent: (_path, options) => {
122
+ capturedSignal = options.signal;
123
+ return request.promise;
124
+ }
125
+ });
126
+
127
+ const pending = controller.loadFileContent('slow.py');
128
+ assert.equal(calls.inlineMessages.at(-1), 'Loading slow.py...');
129
+ assert.equal(capturedSignal instanceof AbortSignal, true);
130
+
131
+ request.resolve({
132
+ render_mode: 'plain',
133
+ html: '<div>ok</div>',
134
+ metadata: { render_note: 'Large file rendered without syntax highlighting.' }
135
+ });
136
+ await pending;
137
+
138
+ assert.equal(calls.renderedPayloads.length, 1);
139
+ assert.equal(dom.fileContentContainer.scrollTop, 0);
140
+ assert.deepEqual(calls.toasts, ['Large file rendered without syntax highlighting.']);
141
+ }
142
+
143
+ {
144
+ const requests = [];
145
+ const { calls, controller } = createFileControllerHarness({
146
+ loadFileContent: (_path, options) => {
147
+ const request = createDeferred();
148
+ request.signal = options.signal;
149
+ requests.push(request);
150
+ return request.promise;
151
+ }
152
+ });
153
+
154
+ const first = controller.loadFileContent('first.py');
155
+ const second = controller.loadFileContent('second.py');
156
+ assert.equal(requests[0].signal.aborted, true);
157
+
158
+ requests[0].resolve({ render_mode: 'plain', html: 'first', metadata: {} });
159
+ await first;
160
+ assert.equal(calls.renderedPayloads.length, 0);
161
+
162
+ requests[1].resolve({ render_mode: 'plain', html: 'second', metadata: {} });
163
+ await second;
164
+ assert.equal(calls.renderedPayloads.length, 1);
165
+ assert.equal(calls.renderedPayloads[0].html, 'second');
166
+ }
167
+
168
+ {
169
+ const { calls, controller } = createFileControllerHarness({
170
+ loadFileContent: () => Promise.reject(new DOMException('Aborted', 'AbortError'))
171
+ });
172
+
173
+ await controller.loadFileContent('aborted.py');
174
+
175
+ assert.deepEqual(calls.inlineMessages, ['Loading aborted.py...']);
176
+ assert.equal(calls.renderedPayloads.length, 0);
177
+ }
178
+ """
179
+
180
+ subprocess.run(
181
+ [node, "--input-type=module", "-e", script],
182
+ cwd=PROJECT_ROOT,
183
+ check=True,
184
+ text=True,
185
+ )