git-juggler 0.2.10__tar.gz → 0.2.11__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 (209) hide show
  1. {git_juggler-0.2.10 → git_juggler-0.2.11}/PKG-INFO +1 -1
  2. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/hook_events.py +201 -49
  3. git_juggler-0.2.11/src/backend/git_juggler/agents/hooks.py +529 -0
  4. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/git_resolver.py +78 -9
  5. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/agents.py +2 -2
  6. git_juggler-0.2.11/src/backend/git_juggler/frontend_dist/assets/index-C-Yz67tX.js +23 -0
  7. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/index.html +1 -1
  8. git_juggler-0.2.11/src/backend/git_juggler/git/gitfiles.py +134 -0
  9. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/main.py +4 -0
  10. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/repo_discovery.py +3 -35
  11. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/themes.py +12 -1
  12. git_juggler-0.2.11/src/backend/tests/hook_event_files.py +24 -0
  13. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_agent_hooks.py +283 -78
  14. git_juggler-0.2.11/src/backend/tests/test_agent_tracking.py +269 -0
  15. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_claude_sessions_data.py +9 -8
  16. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_opencode_sessions.py +13 -13
  17. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_themes.py +15 -0
  18. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Agents/AgentActivityPanel.tsx +3 -3
  19. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Graph/GraphPanel.tsx +9 -11
  20. git_juggler-0.2.11/src/frontend/src/lib/agentRings.ts +75 -0
  21. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/storeCore.ts +27 -2
  22. git_juggler-0.2.11/src/frontend/tests/agentRings.test.ts +95 -0
  23. git_juggler-0.2.10/src/backend/git_juggler/agents/hooks.py +0 -305
  24. git_juggler-0.2.10/src/backend/git_juggler/frontend_dist/assets/index-DWGv6_yw.js +0 -22
  25. git_juggler-0.2.10/src/backend/tests/test_agent_tracking.py +0 -140
  26. {git_juggler-0.2.10 → git_juggler-0.2.11}/.github/workflows/build-package.yml +0 -0
  27. {git_juggler-0.2.10 → git_juggler-0.2.11}/.gitignore +0 -0
  28. {git_juggler-0.2.10 → git_juggler-0.2.11}/CLAUDE.md +0 -0
  29. {git_juggler-0.2.10 → git_juggler-0.2.11}/LICENSE +0 -0
  30. {git_juggler-0.2.10 → git_juggler-0.2.11}/README.md +0 -0
  31. {git_juggler-0.2.10 → git_juggler-0.2.11}/notes.txt +0 -0
  32. {git_juggler-0.2.10 → git_juggler-0.2.11}/pyproject.toml +0 -0
  33. {git_juggler-0.2.10 → git_juggler-0.2.11}/resources/git-juggler-demo-1.gif +0 -0
  34. {git_juggler-0.2.10 → git_juggler-0.2.11}/resources/git-juggler-demo-2.png +0 -0
  35. {git_juggler-0.2.10 → git_juggler-0.2.11}/resources/git-juggler-demo-3.png +0 -0
  36. {git_juggler-0.2.10 → git_juggler-0.2.11}/resources/git-juggler-demo-4.png +0 -0
  37. {git_juggler-0.2.10 → git_juggler-0.2.11}/scripts/build-package.sh +0 -0
  38. {git_juggler-0.2.10 → git_juggler-0.2.11}/scripts/dev.sh +0 -0
  39. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/__init__.py +0 -0
  40. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/__init__.py +0 -0
  41. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/__init__.py +0 -0
  42. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/activity_models.py +0 -0
  43. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/claude_sessions.py +0 -0
  44. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/claude_transcripts.py +0 -0
  45. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/opencode_sessions.py +0 -0
  46. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/paths.py +0 -0
  47. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/text.py +0 -0
  48. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/__init__.py +0 -0
  49. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/ci.py +0 -0
  50. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/config.py +0 -0
  51. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/git.py +0 -0
  52. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/repos.py +0 -0
  53. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/terminal.py +0 -0
  54. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/app.py +0 -0
  55. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ci/__init__.py +0 -0
  56. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ci/github_actions.py +0 -0
  57. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ci/http.py +0 -0
  58. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ci/jenkins.py +0 -0
  59. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ci/run_cache.py +0 -0
  60. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/config.py +0 -0
  61. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/dev_app.py +0 -0
  62. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/agent-logos/claude.png +0 -0
  63. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/agent-logos/opencode.webp +0 -0
  64. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/assets/index-C6HbHDsB.css +0 -0
  65. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/ci-logos/jenkins.webp +0 -0
  66. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/git-juggler.png +0 -0
  67. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/logo.png +0 -0
  68. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/__init__.py +0 -0
  69. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/command.py +0 -0
  70. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/data.py +0 -0
  71. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/detail.py +0 -0
  72. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/diff.py +0 -0
  73. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/refs.py +0 -0
  74. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/status.py +0 -0
  75. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/utils.py +0 -0
  76. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/schemas.py +0 -0
  77. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/terminal.py +0 -0
  78. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ui/__init__.py +0 -0
  79. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ui/folder_picker.py +0 -0
  80. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/conftest.py +0 -0
  81. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_ci.py +0 -0
  82. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_ci_poll.py +0 -0
  83. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_ci_poll_route.py +0 -0
  84. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_file_diff.py +0 -0
  85. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_git_data.py +0 -0
  86. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_github_actions.py +0 -0
  87. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_jenkins.py +0 -0
  88. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_pick_folder.py +0 -0
  89. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_preferences.py +0 -0
  90. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_repos.py +0 -0
  91. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_run_cache.py +0 -0
  92. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/.gitignore +0 -0
  93. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/README.md +0 -0
  94. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/index.html +0 -0
  95. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/package-lock.json +0 -0
  96. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/package.json +0 -0
  97. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/public/agent-logos/claude.png +0 -0
  98. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/public/agent-logos/opencode.webp +0 -0
  99. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/public/ci-logos/jenkins.webp +0 -0
  100. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/public/git-juggler.png +0 -0
  101. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/public/logo.png +0 -0
  102. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/App.css +0 -0
  103. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/App.tsx +0 -0
  104. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/api/client.ts +0 -0
  105. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/api/types.ts +0 -0
  106. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Agents/AgentHoverCard.tsx +0 -0
  107. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Agents/AgentStatusPopover.tsx +0 -0
  108. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Agents/agentFormat.ts +0 -0
  109. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Badges/BranchBadge.tsx +0 -0
  110. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Badges/CiRunBadge.tsx +0 -0
  111. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Badges/StashBadge.tsx +0 -0
  112. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Badges/TagBadge.tsx +0 -0
  113. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Badges/ciStatus.tsx +0 -0
  114. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Branches/DeleteBranchModal.tsx +0 -0
  115. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/CIRunPopover.tsx +0 -0
  116. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/CommandPalette/CommandPalette.tsx +0 -0
  117. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitDetail.tsx +0 -0
  118. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitList.tsx +0 -0
  119. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitRow.tsx +0 -0
  120. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Commits/LineCounts.tsx +0 -0
  121. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Commits/UncommittedRow.tsx +0 -0
  122. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/BranchContextMenu.tsx +0 -0
  123. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CommitContextMenu.tsx +0 -0
  124. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CreateBranchModal.tsx +0 -0
  125. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CreateTagModal.tsx +0 -0
  126. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +0 -0
  127. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/TagContextMenu.tsx +0 -0
  128. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Diff/FileDiffModal.tsx +0 -0
  129. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Graph/branchColor.ts +0 -0
  130. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Graph/computeColumns.ts +0 -0
  131. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Graph/laneWidth.ts +0 -0
  132. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/AgentQuickPicks.tsx +0 -0
  133. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/AgentsSettings.tsx +0 -0
  134. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/CiPollField.tsx +0 -0
  135. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/DirectoryBrowserModal.tsx +0 -0
  136. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/GitHubSettings.tsx +0 -0
  137. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/JenkinsSettings.tsx +0 -0
  138. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/MainMenu.tsx +0 -0
  139. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/RepoPathsSettings.tsx +0 -0
  140. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/ThemePicker.tsx +0 -0
  141. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/ThemeQuickPicks.tsx +0 -0
  142. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +0 -0
  143. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Search/MultiSelect.tsx +0 -0
  144. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Search/SearchBox.tsx +0 -0
  145. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Search/TriSwitch.tsx +0 -0
  146. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/NewGroupModal.tsx +0 -0
  147. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/RepoList.tsx +0 -0
  148. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/Sidebar.tsx +0 -0
  149. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/panelKit.tsx +0 -0
  150. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Tabs/TabsBar.tsx +0 -0
  151. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Tags/DeleteTagModal.tsx +0 -0
  152. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Terminal/TerminalPanel.tsx +0 -0
  153. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Welcome/WelcomeWizard.tsx +0 -0
  154. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/icons.tsx +0 -0
  155. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/inputs/NumberField.tsx +0 -0
  156. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/inputs/TextField.tsx +0 -0
  157. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/inputs/ToggleField.tsx +0 -0
  158. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/inputs/styles.ts +0 -0
  159. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/index.css +0 -0
  160. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/index.tsx +0 -0
  161. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/agentStatusPopoverData.ts +0 -0
  162. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/appTheme.ts +0 -0
  163. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/branchVisibility.ts +0 -0
  164. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/bundledThemes.ts +0 -0
  165. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/ciPoll.ts +0 -0
  166. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/ciRunPopoverData.ts +0 -0
  167. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/color.ts +0 -0
  168. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/dismissOnOutsideClick.ts +0 -0
  169. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/flip.ts +0 -0
  170. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/formatDate.ts +0 -0
  171. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/preferenceSender.ts +0 -0
  172. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/scrollToCommit.ts +0 -0
  173. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/sideBySide.ts +0 -0
  174. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/stageWindow.ts +0 -0
  175. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/stripAnsi.ts +0 -0
  176. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/swallowNextClick.ts +0 -0
  177. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/vscodeTheme.ts +0 -0
  178. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/agents.ts +0 -0
  179. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/ci.ts +0 -0
  180. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/graph.ts +0 -0
  181. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/overlayStack.ts +0 -0
  182. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/overlays.ts +0 -0
  183. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/preferenceSync.ts +0 -0
  184. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/preferences.ts +0 -0
  185. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/repos.ts +0 -0
  186. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/settings.ts +0 -0
  187. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/store.ts +0 -0
  188. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/tabs.ts +0 -0
  189. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/terminal.ts +0 -0
  190. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/themes.ts +0 -0
  191. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/ci.css +0 -0
  192. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/commits.css +0 -0
  193. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/graph.css +0 -0
  194. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/menu.css +0 -0
  195. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/sidebar.css +0 -0
  196. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/terminal.css +0 -0
  197. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/agentFormat.test.ts +0 -0
  198. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/agentStatusPopoverData.test.ts +0 -0
  199. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/ciPoll.test.ts +0 -0
  200. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/ciRunPopoverData.test.ts +0 -0
  201. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/computeColumns.test.ts +0 -0
  202. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/preferenceSender.test.ts +0 -0
  203. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/sideBySide.test.ts +0 -0
  204. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/stageWindow.test.ts +0 -0
  205. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/vscodeTheme.test.ts +0 -0
  206. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tsconfig.app.json +0 -0
  207. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tsconfig.json +0 -0
  208. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tsconfig.node.json +0 -0
  209. {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/vite.config.ts +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: git-juggler
3
- Version: 0.2.10
3
+ Version: 0.2.11
4
4
  Summary: A local Git dashboard for people juggling agents and too many repos.
5
5
  License-Expression: MIT
6
6
  License-File: LICENSE
@@ -10,9 +10,9 @@ import threading
10
10
  import time
11
11
  from dataclasses import dataclass, replace
12
12
  from pathlib import Path
13
- from typing import Any
13
+ from typing import Any, Callable
14
14
 
15
- from .hooks import EVENT_PATH
15
+ from . import hooks
16
16
  from .tracking.activity_models import ActivityEvidence, AgentRepositoryScan, AgentWorktreeActivity, SessionDetails
17
17
  from .tracking.claude_sessions import CLAUDE_SESSIONS_DIR, ClaudeSession, read_registry, registry_signature
18
18
  from .tracking.claude_transcripts import CLAUDE_PROJECTS_DIR, TranscriptInfo, find_transcript, read_transcript_info, transcript_signature
@@ -21,14 +21,18 @@ from .tracking.opencode_sessions import OPENCODE_DB_PATH, OpenCodeSession, db_si
21
21
 
22
22
 
23
23
  HOOK_ACTIVITY_TTL_MS = 120_000
24
- HOOK_EVENT_LIMIT = 5000
25
- SESSION_EVENT_LIMIT = 300
24
+ SESSION_EVENT_LIMIT = hooks.SESSION_EVENT_LIMIT
26
25
  IDLE_SESSION_MAX_MS = 24 * 60 * 60 * 1000
27
- END_PHASES = {"sessionend", "session.deleted"}
26
+ END_PHASES = set(hooks.END_PHASES)
28
27
  # A new user prompt starts a new unit of work: commands run for earlier prompts
29
28
  # stop counting as proof of activity (see _since_last_prompt).
30
- PROMPT_PHASES = {"userpromptsubmit"}
29
+ PROMPT_PHASES = set(hooks.PROMPT_PHASES)
31
30
  HOOK_SCORE = 30
31
+ # Session files are removed once the reader would ignore them anyway: a day
32
+ # after their last write, or shortly after the session ended.
33
+ SESSION_FILE_MAX_AGE_S = IDLE_SESSION_MAX_MS / 1000
34
+ ENDED_SESSION_FILE_GRACE_S = 60
35
+ SESSION_FILE_CLEANUP_INTERVAL_S = 600
32
36
 
33
37
 
34
38
  @dataclass(frozen=True)
@@ -43,6 +47,157 @@ class HookEvent:
43
47
  raw: dict[str, Any]
44
48
 
45
49
 
50
+ class SessionFiles:
51
+ """The per-session event files the hooks write (see hooks.SESSIONS_DIR).
52
+
53
+ Each is small (one turn) and parsed again only when it changes, so a poll
54
+ costs one directory listing plus whatever was written since the last one.
55
+ """
56
+
57
+ def __init__(self, directory: Path | None, parse: Callable[[str], "HookEvent | None"]) -> None:
58
+ self.directory = directory
59
+ self._parse = parse
60
+ self._parsed: dict[str, tuple[tuple[int, int], list[HookEvent]]] = {}
61
+ self._last_cleanup = 0.0
62
+
63
+ def _listing(self) -> list[tuple[str, str, os.stat_result]]:
64
+ if self.directory is None:
65
+ return []
66
+ listing = []
67
+ try:
68
+ with os.scandir(self.directory) as entries:
69
+ for entry in entries:
70
+ if not entry.name.endswith(".jsonl"):
71
+ continue
72
+ try:
73
+ if entry.is_file():
74
+ listing.append((entry.name, entry.path, entry.stat()))
75
+ except OSError:
76
+ continue
77
+ except OSError:
78
+ return []
79
+ return listing
80
+
81
+ def signature(self) -> tuple:
82
+ return tuple(sorted((name, stat.st_mtime_ns, stat.st_size) for name, _, stat in self._listing()))
83
+
84
+ def events(self) -> list[list[HookEvent]]:
85
+ """Each file's events, in the order they were written."""
86
+ by_file: list[list[HookEvent]] = []
87
+ present: set[str] = set()
88
+ for name, path, stat in self._listing():
89
+ key = (stat.st_mtime_ns, stat.st_size)
90
+ cached = self._parsed.get(name)
91
+ if cached is None or cached[0] != key:
92
+ try:
93
+ text = Path(path).read_text(encoding="utf-8", errors="replace")
94
+ except OSError:
95
+ continue
96
+ cached = (key, [event for event in map(self._parse, text.splitlines()) if event is not None])
97
+ self._parsed[name] = cached
98
+ present.add(name)
99
+ by_file.append(cached[1])
100
+ for name in set(self._parsed) - present:
101
+ del self._parsed[name]
102
+ return by_file
103
+
104
+ def cleanup(self, now_s: float) -> None:
105
+ """Deletes files of sessions that ended, and of ones untouched for a day.
106
+
107
+ A failed delete (on Windows, a file a hook has open) is retried on the
108
+ next pass."""
109
+ if self.directory is None or now_s - self._last_cleanup < SESSION_FILE_CLEANUP_INTERVAL_S:
110
+ return
111
+ self._last_cleanup = now_s
112
+ for name, path, stat in self._listing():
113
+ age = now_s - stat.st_mtime
114
+ cached = self._parsed.get(name)
115
+ ended = cached is not None and cached[0] == (stat.st_mtime_ns, stat.st_size) and bool(cached[1]) and cached[1][-1].phase.lower() in END_PHASES
116
+ if age > SESSION_FILE_MAX_AGE_S or (ended and age > ENDED_SESSION_FILE_GRACE_S):
117
+ try:
118
+ os.unlink(path)
119
+ except OSError:
120
+ continue
121
+
122
+
123
+ class LegacyEventLog:
124
+ """The single log hooks appended to before per-session files (hooks.EVENT_PATH).
125
+
126
+ Still read while something writes to it, e.g. an OpenCode started before
127
+ its plugin was upgraded: the whole file once, then only what was appended
128
+ since. Once nothing has written to it for a day it holds nothing the
129
+ reader would use, so it is removed.
130
+ """
131
+
132
+ def __init__(self, path: Path | None, parse: Callable[[str], "HookEvent | None"]) -> None:
133
+ self.path = path
134
+ self._parse = parse
135
+ self._reset(None)
136
+
137
+ def _reset(self, identity: tuple[int, int] | None) -> None:
138
+ self._identity = identity
139
+ self._offset = 0
140
+ self._events: list[HookEvent] = []
141
+
142
+ def signature(self) -> tuple[int, int] | None:
143
+ if self.path is None:
144
+ return None
145
+ try:
146
+ stat = self.path.stat()
147
+ except OSError:
148
+ return None
149
+ return (stat.st_mtime_ns, stat.st_size)
150
+
151
+ def events(self, now_s: float) -> list[HookEvent]:
152
+ if self.path is None:
153
+ return []
154
+ try:
155
+ stat = self.path.stat()
156
+ except OSError:
157
+ self._reset(None)
158
+ return []
159
+ if now_s - stat.st_mtime > SESSION_FILE_MAX_AGE_S:
160
+ try:
161
+ self.path.unlink()
162
+ except OSError:
163
+ pass
164
+ self._reset(None)
165
+ return []
166
+ identity = (stat.st_dev, stat.st_ino)
167
+ if identity != self._identity or stat.st_size < self._offset:
168
+ self._reset(identity)
169
+ if stat.st_size > self._offset:
170
+ try:
171
+ with self.path.open("rb") as file:
172
+ file.seek(self._offset)
173
+ chunk = file.read(stat.st_size - self._offset)
174
+ except OSError:
175
+ return self._events
176
+ # Only whole lines: a line still being written is read next time.
177
+ end = chunk.rfind(b"\n") + 1
178
+ self._offset += end
179
+ self._append(chunk[:end].decode("utf-8", errors="replace").splitlines())
180
+ return self._events
181
+
182
+ def _append(self, lines: list[str]) -> None:
183
+ # Newest first, stopping a day before the newest event: on the first
184
+ # read that skips parsing most of a large file.
185
+ newest = self._events[-1].timestamp if self._events else None
186
+ fresh: list[HookEvent] = []
187
+ for line in reversed(lines):
188
+ event = self._parse(line)
189
+ if event is None:
190
+ continue
191
+ if newest is None or event.timestamp > newest:
192
+ newest = event.timestamp
193
+ if event.timestamp < newest - IDLE_SESSION_MAX_MS:
194
+ break
195
+ fresh.append(event)
196
+ fresh.reverse()
197
+ cutoff = (newest or 0) - IDLE_SESSION_MAX_MS
198
+ self._events = [event for event in self._events if event.timestamp >= cutoff] + fresh
199
+
200
+
46
201
  def _pid_alive(pid: int) -> bool:
47
202
  if sys.platform == "win32":
48
203
  # os.kill(pid, 0) would terminate the process there.
@@ -57,8 +212,11 @@ def _pid_alive(pid: int) -> bool:
57
212
 
58
213
 
59
214
  class AgentHookEventReader:
60
- def __init__(self, event_path: Path = EVENT_PATH, git_resolver: GitResolver | None = None, ttl_ms: int = HOOK_ACTIVITY_TTL_MS, claude_sessions_dir: Path | None = CLAUDE_SESSIONS_DIR, claude_projects_dir: Path | None = CLAUDE_PROJECTS_DIR, opencode_db_path: Path | None = OPENCODE_DB_PATH) -> None:
61
- self.event_path = event_path
215
+ # The event sources default to none: the app passes the real locations
216
+ # (api/agents.py), since this reader deletes stale files in them.
217
+ def __init__(self, sessions_dir: Path | None = None, git_resolver: GitResolver | None = None, ttl_ms: int = HOOK_ACTIVITY_TTL_MS, claude_sessions_dir: Path | None = CLAUDE_SESSIONS_DIR, claude_projects_dir: Path | None = CLAUDE_PROJECTS_DIR, opencode_db_path: Path | None = OPENCODE_DB_PATH, legacy_event_path: Path | None = None) -> None:
218
+ self.session_files = SessionFiles(sessions_dir, self._parse_line)
219
+ self.legacy_log = LegacyEventLog(legacy_event_path, lambda line: self._parse_line(line, slim=True))
62
220
  # None disables the Claude session registry (hooks-only behaviour).
63
221
  self.claude_sessions_dir = claude_sessions_dir
64
222
  self.claude_projects_dir = claude_projects_dir
@@ -84,12 +242,13 @@ class AgentHookEventReader:
84
242
  is the real process, and its busy/idle status overrides the timer.
85
243
 
86
244
  The endpoint is polled every second, so the result is reused while the
87
- events file and the registry are unchanged and nothing could have aged
245
+ session files and the registry are unchanged and nothing could have aged
88
246
  out or moved branch/commit (git resolver TTL). An explicit `now`
89
247
  always recomputes.
90
248
  """
91
249
  observed_at = now or int(time.time() * 1000)
92
- signature = (self._file_signature(), registry_signature(self.claude_sessions_dir), self._transcripts_signature(), db_signature(self.opencode_db_path))
250
+ self.session_files.cleanup(time.time())
251
+ signature = (self.session_files.signature(), self.legacy_log.signature(), registry_signature(self.claude_sessions_dir), self._transcripts_signature(), db_signature(self.opencode_db_path))
93
252
  with self._lock:
94
253
  if now is None and signature == self._cached_signature and observed_at < self._cached_valid_until:
95
254
  return [replace(scan, scanned_at=observed_at) for scan in self._cached_scans]
@@ -120,17 +279,14 @@ class AgentHookEventReader:
120
279
  self._transcript_cache[path] = cached
121
280
  return cached[1]
122
281
 
123
- def _file_signature(self) -> tuple[int, int] | None:
124
- try:
125
- stat = self.event_path.stat()
126
- except OSError:
127
- return None
128
- return (stat.st_mtime_ns, stat.st_size)
129
-
130
282
  def _compute_scans(self, observed_at: int) -> tuple[list[AgentRepositoryScan], int | None]:
131
283
  """Returns the scans plus the time the next active->idle flip happens."""
132
284
  sessions: dict[tuple[str, str], list[HookEvent]] = {}
285
+ # Where each session started: its first event, however old (the hooks
286
+ # keep it as the first line of the session's file).
287
+ started_in: dict[tuple[str, str], str | None] = {}
133
288
  for event in self._read_events():
289
+ started_in.setdefault(self._session_key(event), event.cwd)
134
290
  if observed_at - event.timestamp <= IDLE_SESSION_MAX_MS:
135
291
  sessions.setdefault(self._session_key(event), []).append(event)
136
292
 
@@ -145,8 +301,7 @@ class AgentHookEventReader:
145
301
  continue
146
302
  provider = session_key[0]
147
303
  session_id = session_events[-1].session_id
148
- # Where the session started, taken before trimming to the latest prompt.
149
- session_directory = session_events[0].cwd
304
+ session_directory = started_in.get(session_key)
150
305
  session_events = self._since_last_prompt(session_events)
151
306
  card: ClaudeSession | None = None
152
307
  opencode_session: OpenCodeSession | None = None
@@ -318,38 +473,35 @@ class AgentHookEventReader:
318
473
  return (event.provider, f"cwd:{event.cwd or 'unknown'}")
319
474
 
320
475
  def _read_events(self) -> list[HookEvent]:
321
- if not self.event_path.exists():
322
- return []
323
- try:
324
- lines = self.event_path.read_text(encoding="utf-8", errors="replace").splitlines()[-HOOK_EVENT_LIMIT:]
325
- except OSError:
326
- return []
327
- events: list[HookEvent] = []
328
- for line in lines:
329
- try:
330
- data = json.loads(line)
331
- except json.JSONDecodeError:
332
- continue
333
- if not isinstance(data, dict):
334
- continue
335
- raw = data.get("raw") if isinstance(data.get("raw"), dict) else {}
336
- timestamp = data.get("timestamp")
337
- if not isinstance(timestamp, int):
338
- continue
339
- events.append(
340
- HookEvent(
341
- provider=str(data.get("provider") or "unknown"),
342
- phase=str(data.get("phase") or "unknown"),
343
- session_id=self._session_id(raw),
344
- cwd=data.get("cwd") if isinstance(data.get("cwd"), str) else self._raw_string(raw, "cwd"),
345
- pid=data.get("pid") if isinstance(data.get("pid"), int) else None,
346
- agent_pid=self._agent_pid(data),
347
- timestamp=timestamp,
348
- raw=raw,
349
- )
350
- )
476
+ # The old log first: a session present in both has its older events there.
477
+ events = list(self.legacy_log.events(time.time()))
478
+ for file_events in self.session_files.events():
479
+ events.extend(file_events)
351
480
  return events
352
481
 
482
+ def _parse_line(self, line: str, slim: bool = False) -> HookEvent | None:
483
+ try:
484
+ data = json.loads(line)
485
+ except json.JSONDecodeError:
486
+ return None
487
+ if not isinstance(data, dict):
488
+ return None
489
+ raw = data.get("raw") if isinstance(data.get("raw"), dict) else {}
490
+ timestamp = data.get("timestamp")
491
+ if not isinstance(timestamp, int):
492
+ return None
493
+ return HookEvent(
494
+ provider=str(data.get("provider") or "unknown"),
495
+ phase=str(data.get("phase") or "unknown"),
496
+ session_id=self._session_id(raw),
497
+ cwd=data.get("cwd") if isinstance(data.get("cwd"), str) else self._raw_string(raw, "cwd"),
498
+ pid=data.get("pid") if isinstance(data.get("pid"), int) else None,
499
+ agent_pid=self._agent_pid(data),
500
+ timestamp=timestamp,
501
+ # The old log kept whole payloads (tool output, file contents): keep only what is read.
502
+ raw=hooks.slim_raw(raw) if slim else raw,
503
+ )
504
+
353
505
  def _candidate_paths(self, event: HookEvent) -> list[Path]:
354
506
  paths: list[Path] = []
355
507
  for raw_path in self._raw_paths(event.raw):