git-juggler 0.2.9__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 (217) hide show
  1. {git_juggler-0.2.9 → git_juggler-0.2.11}/PKG-INFO +1 -1
  2. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/hook_events.py +210 -51
  3. git_juggler-0.2.11/src/backend/git_juggler/agents/hooks.py +529 -0
  4. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/activity_models.py +5 -0
  5. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/git_resolver.py +78 -9
  6. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/opencode_sessions.py +6 -2
  7. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/agents.py +7 -2
  8. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/ci.py +14 -6
  9. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/app.py +2 -0
  10. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ci/__init__.py +53 -19
  11. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ci/github_actions.py +67 -5
  12. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ci/jenkins.py +231 -23
  13. git_juggler-0.2.11/src/backend/git_juggler/ci/run_cache.py +214 -0
  14. git_juggler-0.2.11/src/backend/git_juggler/frontend_dist/assets/index-C-Yz67tX.js +23 -0
  15. git_juggler-0.2.11/src/backend/git_juggler/frontend_dist/assets/index-C6HbHDsB.css +1 -0
  16. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/index.html +2 -2
  17. git_juggler-0.2.11/src/backend/git_juggler/git/gitfiles.py +134 -0
  18. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/main.py +4 -0
  19. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/repo_discovery.py +3 -35
  20. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/schemas.py +8 -0
  21. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/themes.py +12 -1
  22. git_juggler-0.2.11/src/backend/tests/conftest.py +16 -0
  23. git_juggler-0.2.11/src/backend/tests/hook_event_files.py +24 -0
  24. git_juggler-0.2.11/src/backend/tests/test_agent_hooks.py +706 -0
  25. git_juggler-0.2.11/src/backend/tests/test_agent_tracking.py +269 -0
  26. git_juggler-0.2.11/src/backend/tests/test_ci.py +113 -0
  27. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_claude_sessions_data.py +9 -8
  28. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_github_actions.py +87 -1
  29. git_juggler-0.2.11/src/backend/tests/test_jenkins.py +554 -0
  30. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_opencode_sessions.py +13 -13
  31. git_juggler-0.2.11/src/backend/tests/test_run_cache.py +77 -0
  32. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_themes.py +15 -0
  33. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/api/client.ts +8 -2
  34. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/api/types.ts +7 -0
  35. git_juggler-0.2.11/src/frontend/src/components/Agents/AgentActivityPanel.tsx +264 -0
  36. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Agents/AgentStatusPopover.tsx +9 -0
  37. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Agents/agentFormat.ts +29 -0
  38. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/CIRunPopover.tsx +20 -8
  39. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Graph/GraphPanel.tsx +9 -11
  40. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/MainMenu.tsx +25 -0
  41. git_juggler-0.2.11/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +308 -0
  42. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/RepoList.tsx +40 -4
  43. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/Sidebar.tsx +2 -1
  44. git_juggler-0.2.11/src/frontend/src/components/Sidebar/panelKit.tsx +188 -0
  45. git_juggler-0.2.11/src/frontend/src/lib/agentRings.ts +75 -0
  46. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/agentStatusPopoverData.ts +2 -0
  47. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/ciRunPopoverData.ts +1 -0
  48. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/ci.ts +1 -0
  49. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/storeCore.ts +41 -6
  50. git_juggler-0.2.11/src/frontend/src/styles/ci.css +53 -0
  51. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/styles/sidebar.css +147 -217
  52. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/agentFormat.test.ts +12 -1
  53. git_juggler-0.2.11/src/frontend/tests/agentRings.test.ts +95 -0
  54. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/agentStatusPopoverData.test.ts +11 -0
  55. git_juggler-0.2.9/src/backend/git_juggler/agents/hooks.py +0 -305
  56. git_juggler-0.2.9/src/backend/git_juggler/frontend_dist/assets/index-AgPjmxnQ.css +0 -1
  57. git_juggler-0.2.9/src/backend/git_juggler/frontend_dist/assets/index-DW1CPHRt.js +0 -22
  58. git_juggler-0.2.9/src/backend/tests/test_agent_hooks.py +0 -456
  59. git_juggler-0.2.9/src/backend/tests/test_agent_tracking.py +0 -140
  60. git_juggler-0.2.9/src/backend/tests/test_ci.py +0 -82
  61. git_juggler-0.2.9/src/backend/tests/test_jenkins.py +0 -275
  62. git_juggler-0.2.9/src/frontend/src/components/Agents/AgentActivityPanel.tsx +0 -85
  63. git_juggler-0.2.9/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +0 -81
  64. git_juggler-0.2.9/src/frontend/src/components/Pipelines/StageStrip.tsx +0 -64
  65. git_juggler-0.2.9/src/frontend/src/styles/ci.css +0 -174
  66. {git_juggler-0.2.9 → git_juggler-0.2.11}/.github/workflows/build-package.yml +0 -0
  67. {git_juggler-0.2.9 → git_juggler-0.2.11}/.gitignore +0 -0
  68. {git_juggler-0.2.9 → git_juggler-0.2.11}/CLAUDE.md +0 -0
  69. {git_juggler-0.2.9 → git_juggler-0.2.11}/LICENSE +0 -0
  70. {git_juggler-0.2.9 → git_juggler-0.2.11}/README.md +0 -0
  71. {git_juggler-0.2.9 → git_juggler-0.2.11}/notes.txt +0 -0
  72. {git_juggler-0.2.9 → git_juggler-0.2.11}/pyproject.toml +0 -0
  73. {git_juggler-0.2.9 → git_juggler-0.2.11}/resources/git-juggler-demo-1.gif +0 -0
  74. {git_juggler-0.2.9 → git_juggler-0.2.11}/resources/git-juggler-demo-2.png +0 -0
  75. {git_juggler-0.2.9 → git_juggler-0.2.11}/resources/git-juggler-demo-3.png +0 -0
  76. {git_juggler-0.2.9 → git_juggler-0.2.11}/resources/git-juggler-demo-4.png +0 -0
  77. {git_juggler-0.2.9 → git_juggler-0.2.11}/scripts/build-package.sh +0 -0
  78. {git_juggler-0.2.9 → git_juggler-0.2.11}/scripts/dev.sh +0 -0
  79. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/__init__.py +0 -0
  80. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/__init__.py +0 -0
  81. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/__init__.py +0 -0
  82. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/claude_sessions.py +0 -0
  83. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/claude_transcripts.py +0 -0
  84. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/paths.py +0 -0
  85. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/text.py +0 -0
  86. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/__init__.py +0 -0
  87. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/config.py +0 -0
  88. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/git.py +0 -0
  89. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/repos.py +0 -0
  90. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/terminal.py +0 -0
  91. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ci/http.py +0 -0
  92. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/config.py +0 -0
  93. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/dev_app.py +0 -0
  94. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/agent-logos/claude.png +0 -0
  95. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/agent-logos/opencode.webp +0 -0
  96. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/ci-logos/jenkins.webp +0 -0
  97. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/git-juggler.png +0 -0
  98. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/logo.png +0 -0
  99. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/__init__.py +0 -0
  100. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/command.py +0 -0
  101. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/data.py +0 -0
  102. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/detail.py +0 -0
  103. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/diff.py +0 -0
  104. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/refs.py +0 -0
  105. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/status.py +0 -0
  106. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/utils.py +0 -0
  107. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/terminal.py +0 -0
  108. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ui/__init__.py +0 -0
  109. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ui/folder_picker.py +0 -0
  110. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_ci_poll.py +0 -0
  111. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_ci_poll_route.py +0 -0
  112. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_file_diff.py +0 -0
  113. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_git_data.py +0 -0
  114. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_pick_folder.py +0 -0
  115. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_preferences.py +0 -0
  116. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_repos.py +0 -0
  117. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/.gitignore +0 -0
  118. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/README.md +0 -0
  119. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/index.html +0 -0
  120. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/package-lock.json +0 -0
  121. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/package.json +0 -0
  122. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/public/agent-logos/claude.png +0 -0
  123. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/public/agent-logos/opencode.webp +0 -0
  124. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/public/ci-logos/jenkins.webp +0 -0
  125. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/public/git-juggler.png +0 -0
  126. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/public/logo.png +0 -0
  127. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/App.css +0 -0
  128. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/App.tsx +0 -0
  129. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Agents/AgentHoverCard.tsx +0 -0
  130. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Badges/BranchBadge.tsx +0 -0
  131. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Badges/CiRunBadge.tsx +0 -0
  132. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Badges/StashBadge.tsx +0 -0
  133. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Badges/TagBadge.tsx +0 -0
  134. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Badges/ciStatus.tsx +0 -0
  135. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Branches/DeleteBranchModal.tsx +0 -0
  136. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/CommandPalette/CommandPalette.tsx +0 -0
  137. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitDetail.tsx +0 -0
  138. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitList.tsx +0 -0
  139. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitRow.tsx +0 -0
  140. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Commits/LineCounts.tsx +0 -0
  141. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Commits/UncommittedRow.tsx +0 -0
  142. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/BranchContextMenu.tsx +0 -0
  143. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CommitContextMenu.tsx +0 -0
  144. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CreateBranchModal.tsx +0 -0
  145. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CreateTagModal.tsx +0 -0
  146. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +0 -0
  147. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/TagContextMenu.tsx +0 -0
  148. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Diff/FileDiffModal.tsx +0 -0
  149. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Graph/branchColor.ts +0 -0
  150. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Graph/computeColumns.ts +0 -0
  151. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Graph/laneWidth.ts +0 -0
  152. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/AgentQuickPicks.tsx +0 -0
  153. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/AgentsSettings.tsx +0 -0
  154. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/CiPollField.tsx +0 -0
  155. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/DirectoryBrowserModal.tsx +0 -0
  156. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/GitHubSettings.tsx +0 -0
  157. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/JenkinsSettings.tsx +0 -0
  158. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/RepoPathsSettings.tsx +0 -0
  159. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/ThemePicker.tsx +0 -0
  160. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/ThemeQuickPicks.tsx +0 -0
  161. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Search/MultiSelect.tsx +0 -0
  162. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Search/SearchBox.tsx +0 -0
  163. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Search/TriSwitch.tsx +0 -0
  164. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/NewGroupModal.tsx +0 -0
  165. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Tabs/TabsBar.tsx +0 -0
  166. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Tags/DeleteTagModal.tsx +0 -0
  167. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Terminal/TerminalPanel.tsx +0 -0
  168. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Welcome/WelcomeWizard.tsx +0 -0
  169. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/icons.tsx +0 -0
  170. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/inputs/NumberField.tsx +0 -0
  171. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/inputs/TextField.tsx +0 -0
  172. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/inputs/ToggleField.tsx +0 -0
  173. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/inputs/styles.ts +0 -0
  174. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/index.css +0 -0
  175. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/index.tsx +0 -0
  176. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/appTheme.ts +0 -0
  177. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/branchVisibility.ts +0 -0
  178. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/bundledThemes.ts +0 -0
  179. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/ciPoll.ts +0 -0
  180. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/color.ts +0 -0
  181. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/dismissOnOutsideClick.ts +0 -0
  182. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/flip.ts +0 -0
  183. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/formatDate.ts +0 -0
  184. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/preferenceSender.ts +0 -0
  185. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/scrollToCommit.ts +0 -0
  186. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/sideBySide.ts +0 -0
  187. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/stageWindow.ts +0 -0
  188. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/stripAnsi.ts +0 -0
  189. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/swallowNextClick.ts +0 -0
  190. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/vscodeTheme.ts +0 -0
  191. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/agents.ts +0 -0
  192. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/graph.ts +0 -0
  193. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/overlayStack.ts +0 -0
  194. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/overlays.ts +0 -0
  195. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/preferenceSync.ts +0 -0
  196. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/preferences.ts +0 -0
  197. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/repos.ts +0 -0
  198. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/settings.ts +0 -0
  199. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/store.ts +0 -0
  200. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/tabs.ts +0 -0
  201. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/terminal.ts +0 -0
  202. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/themes.ts +0 -0
  203. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/styles/commits.css +0 -0
  204. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/styles/graph.css +0 -0
  205. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/styles/menu.css +0 -0
  206. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/styles/terminal.css +0 -0
  207. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/ciPoll.test.ts +0 -0
  208. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/ciRunPopoverData.test.ts +0 -0
  209. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/computeColumns.test.ts +0 -0
  210. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/preferenceSender.test.ts +0 -0
  211. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/sideBySide.test.ts +0 -0
  212. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/stageWindow.test.ts +0 -0
  213. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/vscodeTheme.test.ts +0 -0
  214. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tsconfig.app.json +0 -0
  215. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tsconfig.json +0 -0
  216. {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tsconfig.node.json +0 -0
  217. {git_juggler-0.2.9 → 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.9
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,11 +301,11 @@ 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
308
+ parent_opencode_session: OpenCodeSession | None = None
153
309
  busy: bool | None = None # what the agent itself reports, when it does
154
310
  waiting_for: str | None = None
155
311
  if provider == "claude" and registry is not None and session_id:
@@ -169,9 +325,10 @@ class AgentHookEventReader:
169
325
  if not session_id:
170
326
  continue # the plugin's start event: an instance, not yet a session
171
327
  opencode_session = self._opencode_session(session_id, opencode_signature)
172
- # Deleted, archived, or a sub-agent's session (its parent is what the user sees).
173
- if opencode_session is None or opencode_session.archived or opencode_session.is_child:
328
+ if opencode_session is None or opencode_session.archived:
174
329
  continue
330
+ if opencode_session.parent_id:
331
+ parent_opencode_session = self._opencode_session(opencode_session.parent_id, opencode_signature)
175
332
  busy = opencode_session.busy
176
333
 
177
334
  activities: dict[str, AgentWorktreeActivity] = {}
@@ -240,6 +397,11 @@ class AgentHookEventReader:
240
397
  name=card.name if card is not None else (opencode_session.title if opencode_session is not None else None),
241
398
  details=details,
242
399
  waiting_for=waiting_for,
400
+ is_subagent=opencode_session.is_child if opencode_session is not None else False,
401
+ parent_session_id=opencode_session.parent_id if opencode_session is not None else None,
402
+ parent_title=parent_opencode_session.title if parent_opencode_session is not None else None,
403
+ parent_agent=parent_opencode_session.agent if parent_opencode_session is not None else None,
404
+ parent_provider=provider if parent_opencode_session is not None else None,
243
405
  )
244
406
  )
245
407
  return scans, next_change
@@ -311,38 +473,35 @@ class AgentHookEventReader:
311
473
  return (event.provider, f"cwd:{event.cwd or 'unknown'}")
312
474
 
313
475
  def _read_events(self) -> list[HookEvent]:
314
- if not self.event_path.exists():
315
- return []
316
- try:
317
- lines = self.event_path.read_text(encoding="utf-8", errors="replace").splitlines()[-HOOK_EVENT_LIMIT:]
318
- except OSError:
319
- return []
320
- events: list[HookEvent] = []
321
- for line in lines:
322
- try:
323
- data = json.loads(line)
324
- except json.JSONDecodeError:
325
- continue
326
- if not isinstance(data, dict):
327
- continue
328
- raw = data.get("raw") if isinstance(data.get("raw"), dict) else {}
329
- timestamp = data.get("timestamp")
330
- if not isinstance(timestamp, int):
331
- continue
332
- events.append(
333
- HookEvent(
334
- provider=str(data.get("provider") or "unknown"),
335
- phase=str(data.get("phase") or "unknown"),
336
- session_id=self._session_id(raw),
337
- cwd=data.get("cwd") if isinstance(data.get("cwd"), str) else self._raw_string(raw, "cwd"),
338
- pid=data.get("pid") if isinstance(data.get("pid"), int) else None,
339
- agent_pid=self._agent_pid(data),
340
- timestamp=timestamp,
341
- raw=raw,
342
- )
343
- )
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)
344
480
  return events
345
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
+
346
505
  def _candidate_paths(self, event: HookEvent) -> list[Path]:
347
506
  paths: list[Path] = []
348
507
  for raw_path in self._raw_paths(event.raw):