git-juggler 0.2.9__tar.gz → 0.2.10__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.9 → git_juggler-0.2.10}/PKG-INFO +1 -1
  2. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/hook_events.py +9 -2
  3. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/hooks.py +2 -2
  4. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/tracking/activity_models.py +5 -0
  5. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/tracking/opencode_sessions.py +6 -2
  6. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/api/agents.py +5 -0
  7. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/api/ci.py +14 -6
  8. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/app.py +2 -0
  9. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/ci/__init__.py +53 -19
  10. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/ci/github_actions.py +67 -5
  11. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/ci/jenkins.py +231 -23
  12. git_juggler-0.2.10/src/backend/git_juggler/ci/run_cache.py +214 -0
  13. git_juggler-0.2.10/src/backend/git_juggler/frontend_dist/assets/index-C6HbHDsB.css +1 -0
  14. git_juggler-0.2.9/src/backend/git_juggler/frontend_dist/assets/index-DW1CPHRt.js → git_juggler-0.2.10/src/backend/git_juggler/frontend_dist/assets/index-DWGv6_yw.js +15 -15
  15. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/frontend_dist/index.html +2 -2
  16. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/schemas.py +8 -0
  17. git_juggler-0.2.10/src/backend/tests/conftest.py +16 -0
  18. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_agent_hooks.py +45 -0
  19. git_juggler-0.2.10/src/backend/tests/test_ci.py +113 -0
  20. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_github_actions.py +87 -1
  21. git_juggler-0.2.10/src/backend/tests/test_jenkins.py +554 -0
  22. git_juggler-0.2.10/src/backend/tests/test_run_cache.py +77 -0
  23. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/api/client.ts +8 -2
  24. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/api/types.ts +7 -0
  25. git_juggler-0.2.10/src/frontend/src/components/Agents/AgentActivityPanel.tsx +264 -0
  26. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Agents/AgentStatusPopover.tsx +9 -0
  27. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Agents/agentFormat.ts +29 -0
  28. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/CIRunPopover.tsx +20 -8
  29. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Menu/MainMenu.tsx +25 -0
  30. git_juggler-0.2.10/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +308 -0
  31. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Sidebar/RepoList.tsx +40 -4
  32. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Sidebar/Sidebar.tsx +2 -1
  33. git_juggler-0.2.10/src/frontend/src/components/Sidebar/panelKit.tsx +188 -0
  34. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/agentStatusPopoverData.ts +2 -0
  35. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/ciRunPopoverData.ts +1 -0
  36. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/ci.ts +1 -0
  37. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/storeCore.ts +14 -4
  38. git_juggler-0.2.10/src/frontend/src/styles/ci.css +53 -0
  39. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/styles/sidebar.css +147 -217
  40. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tests/agentFormat.test.ts +12 -1
  41. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tests/agentStatusPopoverData.test.ts +11 -0
  42. git_juggler-0.2.9/src/backend/git_juggler/frontend_dist/assets/index-AgPjmxnQ.css +0 -1
  43. git_juggler-0.2.9/src/backend/tests/test_ci.py +0 -82
  44. git_juggler-0.2.9/src/backend/tests/test_jenkins.py +0 -275
  45. git_juggler-0.2.9/src/frontend/src/components/Agents/AgentActivityPanel.tsx +0 -85
  46. git_juggler-0.2.9/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +0 -81
  47. git_juggler-0.2.9/src/frontend/src/components/Pipelines/StageStrip.tsx +0 -64
  48. git_juggler-0.2.9/src/frontend/src/styles/ci.css +0 -174
  49. {git_juggler-0.2.9 → git_juggler-0.2.10}/.github/workflows/build-package.yml +0 -0
  50. {git_juggler-0.2.9 → git_juggler-0.2.10}/.gitignore +0 -0
  51. {git_juggler-0.2.9 → git_juggler-0.2.10}/CLAUDE.md +0 -0
  52. {git_juggler-0.2.9 → git_juggler-0.2.10}/LICENSE +0 -0
  53. {git_juggler-0.2.9 → git_juggler-0.2.10}/README.md +0 -0
  54. {git_juggler-0.2.9 → git_juggler-0.2.10}/notes.txt +0 -0
  55. {git_juggler-0.2.9 → git_juggler-0.2.10}/pyproject.toml +0 -0
  56. {git_juggler-0.2.9 → git_juggler-0.2.10}/resources/git-juggler-demo-1.gif +0 -0
  57. {git_juggler-0.2.9 → git_juggler-0.2.10}/resources/git-juggler-demo-2.png +0 -0
  58. {git_juggler-0.2.9 → git_juggler-0.2.10}/resources/git-juggler-demo-3.png +0 -0
  59. {git_juggler-0.2.9 → git_juggler-0.2.10}/resources/git-juggler-demo-4.png +0 -0
  60. {git_juggler-0.2.9 → git_juggler-0.2.10}/scripts/build-package.sh +0 -0
  61. {git_juggler-0.2.9 → git_juggler-0.2.10}/scripts/dev.sh +0 -0
  62. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/__init__.py +0 -0
  63. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/__init__.py +0 -0
  64. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/tracking/__init__.py +0 -0
  65. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/tracking/claude_sessions.py +0 -0
  66. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/tracking/claude_transcripts.py +0 -0
  67. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/tracking/git_resolver.py +0 -0
  68. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/tracking/paths.py +0 -0
  69. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/agents/tracking/text.py +0 -0
  70. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/api/__init__.py +0 -0
  71. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/api/config.py +0 -0
  72. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/api/git.py +0 -0
  73. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/api/repos.py +0 -0
  74. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/api/terminal.py +0 -0
  75. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/ci/http.py +0 -0
  76. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/config.py +0 -0
  77. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/dev_app.py +0 -0
  78. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/frontend_dist/agent-logos/claude.png +0 -0
  79. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/frontend_dist/agent-logos/opencode.webp +0 -0
  80. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/frontend_dist/ci-logos/jenkins.webp +0 -0
  81. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/frontend_dist/git-juggler.png +0 -0
  82. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/frontend_dist/logo.png +0 -0
  83. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/git/__init__.py +0 -0
  84. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/git/command.py +0 -0
  85. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/git/data.py +0 -0
  86. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/git/detail.py +0 -0
  87. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/git/diff.py +0 -0
  88. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/git/refs.py +0 -0
  89. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/git/status.py +0 -0
  90. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/git/utils.py +0 -0
  91. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/main.py +0 -0
  92. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/repo_discovery.py +0 -0
  93. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/terminal.py +0 -0
  94. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/themes.py +0 -0
  95. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/ui/__init__.py +0 -0
  96. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/git_juggler/ui/folder_picker.py +0 -0
  97. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_agent_tracking.py +0 -0
  98. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_ci_poll.py +0 -0
  99. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_ci_poll_route.py +0 -0
  100. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_claude_sessions_data.py +0 -0
  101. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_file_diff.py +0 -0
  102. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_git_data.py +0 -0
  103. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_opencode_sessions.py +0 -0
  104. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_pick_folder.py +0 -0
  105. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_preferences.py +0 -0
  106. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_repos.py +0 -0
  107. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/backend/tests/test_themes.py +0 -0
  108. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/.gitignore +0 -0
  109. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/README.md +0 -0
  110. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/index.html +0 -0
  111. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/package-lock.json +0 -0
  112. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/package.json +0 -0
  113. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/public/agent-logos/claude.png +0 -0
  114. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/public/agent-logos/opencode.webp +0 -0
  115. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/public/ci-logos/jenkins.webp +0 -0
  116. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/public/git-juggler.png +0 -0
  117. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/public/logo.png +0 -0
  118. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/App.css +0 -0
  119. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/App.tsx +0 -0
  120. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Agents/AgentHoverCard.tsx +0 -0
  121. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Badges/BranchBadge.tsx +0 -0
  122. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Badges/CiRunBadge.tsx +0 -0
  123. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Badges/StashBadge.tsx +0 -0
  124. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Badges/TagBadge.tsx +0 -0
  125. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Badges/ciStatus.tsx +0 -0
  126. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Branches/DeleteBranchModal.tsx +0 -0
  127. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/CommandPalette/CommandPalette.tsx +0 -0
  128. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Commits/CommitDetail.tsx +0 -0
  129. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Commits/CommitList.tsx +0 -0
  130. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Commits/CommitRow.tsx +0 -0
  131. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Commits/LineCounts.tsx +0 -0
  132. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Commits/UncommittedRow.tsx +0 -0
  133. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/ContextMenu/BranchContextMenu.tsx +0 -0
  134. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/ContextMenu/CommitContextMenu.tsx +0 -0
  135. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/ContextMenu/CreateBranchModal.tsx +0 -0
  136. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/ContextMenu/CreateTagModal.tsx +0 -0
  137. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +0 -0
  138. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/ContextMenu/TagContextMenu.tsx +0 -0
  139. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Diff/FileDiffModal.tsx +0 -0
  140. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Graph/GraphPanel.tsx +0 -0
  141. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Graph/branchColor.ts +0 -0
  142. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Graph/computeColumns.ts +0 -0
  143. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Graph/laneWidth.ts +0 -0
  144. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Menu/AgentQuickPicks.tsx +0 -0
  145. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Menu/AgentsSettings.tsx +0 -0
  146. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Menu/CiPollField.tsx +0 -0
  147. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Menu/DirectoryBrowserModal.tsx +0 -0
  148. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Menu/GitHubSettings.tsx +0 -0
  149. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Menu/JenkinsSettings.tsx +0 -0
  150. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Menu/RepoPathsSettings.tsx +0 -0
  151. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Menu/ThemePicker.tsx +0 -0
  152. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Menu/ThemeQuickPicks.tsx +0 -0
  153. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Search/MultiSelect.tsx +0 -0
  154. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Search/SearchBox.tsx +0 -0
  155. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Search/TriSwitch.tsx +0 -0
  156. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Sidebar/NewGroupModal.tsx +0 -0
  157. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Tabs/TabsBar.tsx +0 -0
  158. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Tags/DeleteTagModal.tsx +0 -0
  159. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Terminal/TerminalPanel.tsx +0 -0
  160. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/Welcome/WelcomeWizard.tsx +0 -0
  161. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/icons.tsx +0 -0
  162. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/inputs/NumberField.tsx +0 -0
  163. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/inputs/TextField.tsx +0 -0
  164. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/inputs/ToggleField.tsx +0 -0
  165. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/components/inputs/styles.ts +0 -0
  166. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/index.css +0 -0
  167. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/index.tsx +0 -0
  168. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/appTheme.ts +0 -0
  169. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/branchVisibility.ts +0 -0
  170. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/bundledThemes.ts +0 -0
  171. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/ciPoll.ts +0 -0
  172. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/color.ts +0 -0
  173. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/dismissOnOutsideClick.ts +0 -0
  174. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/flip.ts +0 -0
  175. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/formatDate.ts +0 -0
  176. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/preferenceSender.ts +0 -0
  177. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/scrollToCommit.ts +0 -0
  178. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/sideBySide.ts +0 -0
  179. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/stageWindow.ts +0 -0
  180. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/stripAnsi.ts +0 -0
  181. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/swallowNextClick.ts +0 -0
  182. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/lib/vscodeTheme.ts +0 -0
  183. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/agents.ts +0 -0
  184. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/graph.ts +0 -0
  185. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/overlayStack.ts +0 -0
  186. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/overlays.ts +0 -0
  187. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/preferenceSync.ts +0 -0
  188. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/preferences.ts +0 -0
  189. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/repos.ts +0 -0
  190. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/settings.ts +0 -0
  191. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/store.ts +0 -0
  192. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/tabs.ts +0 -0
  193. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/terminal.ts +0 -0
  194. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/state/themes.ts +0 -0
  195. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/styles/commits.css +0 -0
  196. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/styles/graph.css +0 -0
  197. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/styles/menu.css +0 -0
  198. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/src/styles/terminal.css +0 -0
  199. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tests/ciPoll.test.ts +0 -0
  200. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tests/ciRunPopoverData.test.ts +0 -0
  201. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tests/computeColumns.test.ts +0 -0
  202. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tests/preferenceSender.test.ts +0 -0
  203. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tests/sideBySide.test.ts +0 -0
  204. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tests/stageWindow.test.ts +0 -0
  205. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tests/vscodeTheme.test.ts +0 -0
  206. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tsconfig.app.json +0 -0
  207. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tsconfig.json +0 -0
  208. {git_juggler-0.2.9 → git_juggler-0.2.10}/src/frontend/tsconfig.node.json +0 -0
  209. {git_juggler-0.2.9 → git_juggler-0.2.10}/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.10
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
@@ -150,6 +150,7 @@ class AgentHookEventReader:
150
150
  session_events = self._since_last_prompt(session_events)
151
151
  card: ClaudeSession | None = None
152
152
  opencode_session: OpenCodeSession | None = None
153
+ parent_opencode_session: OpenCodeSession | None = None
153
154
  busy: bool | None = None # what the agent itself reports, when it does
154
155
  waiting_for: str | None = None
155
156
  if provider == "claude" and registry is not None and session_id:
@@ -169,9 +170,10 @@ class AgentHookEventReader:
169
170
  if not session_id:
170
171
  continue # the plugin's start event: an instance, not yet a session
171
172
  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:
173
+ if opencode_session is None or opencode_session.archived:
174
174
  continue
175
+ if opencode_session.parent_id:
176
+ parent_opencode_session = self._opencode_session(opencode_session.parent_id, opencode_signature)
175
177
  busy = opencode_session.busy
176
178
 
177
179
  activities: dict[str, AgentWorktreeActivity] = {}
@@ -240,6 +242,11 @@ class AgentHookEventReader:
240
242
  name=card.name if card is not None else (opencode_session.title if opencode_session is not None else None),
241
243
  details=details,
242
244
  waiting_for=waiting_for,
245
+ is_subagent=opencode_session.is_child if opencode_session is not None else False,
246
+ parent_session_id=opencode_session.parent_id if opencode_session is not None else None,
247
+ parent_title=parent_opencode_session.title if parent_opencode_session is not None else None,
248
+ parent_agent=parent_opencode_session.agent if parent_opencode_session is not None else None,
249
+ parent_provider=provider if parent_opencode_session is not None else None,
243
250
  )
244
251
  )
245
252
  return scans, next_change
@@ -102,8 +102,8 @@ def _claude_snippet_dict() -> dict:
102
102
  "hooks": {
103
103
  "SessionStart": [_claude_hook_entry("SessionStart")],
104
104
  "UserPromptSubmit": [_claude_hook_entry("UserPromptSubmit")],
105
- "PreToolUse": [_claude_hook_entry("PreToolUse", "Bash|Edit|MultiEdit|Write|Read|Glob|Grep|LS")],
106
- "PostToolUse": [_claude_hook_entry("PostToolUse", "Bash|Edit|MultiEdit|Write|Read|Glob|Grep|LS")],
105
+ "PreToolUse": [_claude_hook_entry("PreToolUse", "Bash|Edit|MultiEdit|Write|Read|Glob|Grep|LS|Task")],
106
+ "PostToolUse": [_claude_hook_entry("PostToolUse", "Bash|Edit|MultiEdit|Write|Read|Glob|Grep|LS|Task")],
107
107
  "SessionEnd": [_claude_hook_entry("SessionEnd")],
108
108
  }
109
109
  }
@@ -63,3 +63,8 @@ class AgentRepositoryScan:
63
63
  details: SessionDetails | None = None
64
64
  # What the agent is blocked on ("permission prompt", "input needed"), or None.
65
65
  waiting_for: str | None = None
66
+ is_subagent: bool = False
67
+ parent_session_id: str | None = None
68
+ parent_title: str | None = None
69
+ parent_agent: str | None = None
70
+ parent_provider: str | None = None
@@ -27,10 +27,14 @@ class OpenCodeSession:
27
27
  time_created: int | None
28
28
  time_updated: int | None
29
29
  archived: bool
30
- is_child: bool
30
+ parent_id: str | None
31
31
  busy: bool
32
32
  last_prompt: str | None
33
33
 
34
+ @property
35
+ def is_child(self) -> bool:
36
+ return self.parent_id is not None
37
+
34
38
 
35
39
  def db_signature(path: Path | None) -> tuple | None:
36
40
  """Change marker for the DB (OpenCode uses WAL, so the -wal file moves first).
@@ -117,7 +121,7 @@ def read_session(path: Path | None, session_id: str, now_ms: int | None = None)
117
121
  time_created=time_created if isinstance(time_created, int) else None,
118
122
  time_updated=time_updated if isinstance(time_updated, int) else None,
119
123
  archived=time_archived is not None,
120
- is_child=parent_id is not None,
124
+ parent_id=parent_id if isinstance(parent_id, str) else None,
121
125
  busy=busy,
122
126
  last_prompt=last_prompt,
123
127
  )
@@ -46,6 +46,11 @@ def _agent_scan_response(scan: AgentRepositoryScan) -> AgentRepositoryScanRespon
46
46
  name=scan.name,
47
47
  details=scan.details.__dict__ if scan.details is not None else None,
48
48
  waiting_for=scan.waiting_for,
49
+ is_subagent=scan.is_subagent,
50
+ parent_session_id=scan.parent_session_id,
51
+ parent_title=scan.parent_title,
52
+ parent_agent=scan.parent_agent,
53
+ parent_provider=scan.parent_provider,
49
54
  )
50
55
 
51
56
 
@@ -6,8 +6,8 @@ from typing import Annotated
6
6
  from fastapi import APIRouter, HTTPException, Query
7
7
 
8
8
  from .. import config
9
- from ..ci import github_actions, jenkins
10
- from ..ci import get_active_pipelines, get_ci_run_stages, get_ci_runs, poll_ci_runs
9
+ from ..ci import github_actions, jenkins, run_cache
10
+ from ..ci import get_ci_run_stages, get_ci_runs, get_recent_pipelines, poll_ci_runs
11
11
  from ..git.data import get_commit_hashes
12
12
  from ..repo_discovery import list_repos, resolve_repo_path
13
13
  from ..schemas import (
@@ -77,16 +77,24 @@ def api_ci_stages(repo_id: str, provider: str, run_id: str) -> list[CiStage]:
77
77
  return stages
78
78
 
79
79
 
80
- # Queued/running pipelines across all repos, for the Pipelines tab.
81
- @router.get("/api/ci/active", response_model=list[ActivePipeline])
82
- def api_ci_active() -> list[ActivePipeline]:
83
- return get_active_pipelines(
80
+ # The most recent pipelines (running or finished) across all repos, for the
81
+ # Pipelines tab.
82
+ @router.get("/api/ci/recent", response_model=list[ActivePipeline])
83
+ def api_ci_recent(limit: Annotated[int, Query(ge=1, le=50)] = 10) -> list[ActivePipeline]:
84
+ return get_recent_pipelines(
84
85
  list_repos(config.load_repo_paths()),
85
86
  config.load_github_config(),
86
87
  config.load_jenkins_config(),
88
+ limit,
87
89
  )
88
90
 
89
91
 
92
+ # Forget every stored finished run (and runs the CI servers no longer have).
93
+ @router.post("/api/ci/cache/clear", status_code=204)
94
+ def api_ci_cache_clear() -> None:
95
+ run_cache.clear()
96
+
97
+
90
98
  @router.post("/api/ci/github/test", response_model=CiConnectionTestResponse)
91
99
  def api_test_github_connection(body: GitHubConfig) -> CiConnectionTestResponse:
92
100
  ok, message = github_actions.test_connection(body.model_dump())
@@ -14,12 +14,14 @@ from .api.config import create_config_router
14
14
  from .api.git import router as git_router
15
15
  from .api.repos import router as repos_router
16
16
  from .api.terminal import create_terminal_router
17
+ from .ci import run_cache
17
18
 
18
19
 
19
20
  def create_app(root_path: Path, frontend_dist: Path | None = None) -> FastAPI:
20
21
  app = FastAPI(title="git-juggler")
21
22
  app.state.root_path = root_path
22
23
  config.ensure_seeded(root_path)
24
+ run_cache.configure(config.CONFIG_DIR / "ci-cache.sqlite")
23
25
 
24
26
  # Only needed for local dev, when the Vite dev server (a different origin)
25
27
  # talks to this API directly instead of through its proxy.
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from concurrent.futures import ThreadPoolExecutor
4
4
  from dataclasses import dataclass
5
+ from datetime import datetime, timezone
5
6
  from pathlib import Path
6
7
  from typing import Protocol
7
8
 
@@ -27,6 +28,11 @@ class CiProvider(Protocol):
27
28
  Stages are included for runs that are still going."""
28
29
  ...
29
30
 
31
+ def recent_runs(self, repo_path: Path, limit: int) -> list[CiRunInfo]:
32
+ """The repo's latest runs (at most `limit` per job/workflow listing),
33
+ running or finished, without stages."""
34
+ ...
35
+
30
36
 
31
37
  @dataclass
32
38
  class GitHubActionsProvider:
@@ -39,6 +45,9 @@ class GitHubActionsProvider:
39
45
  def poll_runs(self, repo_path: Path, run_ids: list[str] | None, head_sha: str | None, branch_name: str | None) -> list[CiRunInfo]:
40
46
  return github_actions.poll_runs(repo_path, self.config, run_ids, head_sha)
41
47
 
48
+ def recent_runs(self, repo_path: Path, limit: int) -> list[CiRunInfo]:
49
+ return github_actions.get_recent_runs(repo_path, self.config, limit)
50
+
42
51
 
43
52
  @dataclass
44
53
  class JenkinsProvider:
@@ -51,6 +60,9 @@ class JenkinsProvider:
51
60
  def poll_runs(self, repo_path: Path, run_ids: list[str] | None, head_sha: str | None, branch_name: str | None) -> list[CiRunInfo]:
52
61
  return jenkins.poll_runs(repo_path, self.config, run_ids, head_sha, branch_name)
53
62
 
63
+ def recent_runs(self, repo_path: Path, limit: int) -> list[CiRunInfo]:
64
+ return jenkins.get_recent_builds(repo_path, self.config, limit)
65
+
54
66
 
55
67
  def enabled_providers(github_config: dict | None, jenkins_config: dict | None) -> list[CiProvider]:
56
68
  providers: list[CiProvider] = []
@@ -116,32 +128,54 @@ def get_ci_run_stages(
116
128
  return None
117
129
 
118
130
 
119
- def _active_runs_for_repo(repo: RepoSummary, github_config: dict | None, jenkins_config: dict | None) -> list[ActivePipeline]:
120
- runs = poll_ci_runs(Path(repo.path), None, None, None, github_config, jenkins_config)
121
- return [ActivePipeline(repo_id=repo.id, repo_name=repo.name, run=run) for run in runs]
131
+ def _recent_runs_for_repo(
132
+ repo: RepoSummary, github_config: dict | None, jenkins_config: dict | None, limit: int
133
+ ) -> list[tuple[RepoSummary, CiRunInfo]]:
134
+ providers = enabled_providers(github_config, jenkins_config)
135
+ return [(repo, run) for provider in providers for run in provider.recent_runs(Path(repo.path), limit)]
136
+
122
137
 
138
+ def _run_key(run: CiRunInfo) -> tuple[str, str]:
139
+ return (run.provider, run.run_id or run.url)
123
140
 
124
- def _pipeline_key(pipeline: ActivePipeline) -> tuple[str, str]:
125
- run = pipeline.run
126
- if run.run_id:
127
- return (run.provider, run.run_id)
128
- return (run.provider, run.url)
129
141
 
142
+ def _created_at_key(run: CiRunInfo) -> datetime:
143
+ # Providers format timestamps differently ("Z" vs "+00:00"), so compare
144
+ # parsed values rather than strings.
145
+ try:
146
+ parsed = datetime.fromisoformat((run.created_at or "").replace("Z", "+00:00"))
147
+ except ValueError:
148
+ return datetime.min.replace(tzinfo=timezone.utc)
149
+ return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc)
130
150
 
131
- def get_active_pipelines(
151
+
152
+ def get_recent_pipelines(
132
153
  repos: list[RepoSummary],
133
154
  github_config: dict | None,
134
155
  jenkins_config: dict | None,
156
+ limit: int = 10,
135
157
  ) -> list[ActivePipeline]:
136
- """Queued/running pipelines across every known repo. Repos are queried
137
- concurrently since each one is a network round trip."""
138
- if not repos:
158
+ """The `limit` most recent pipelines across every known repo, running or
159
+ finished, newest first, with their stages. Repos are queried concurrently
160
+ since each one is a network round trip; stages are only fetched for the
161
+ runs that make the cut."""
162
+ if not repos or limit <= 0:
139
163
  return []
140
164
  with ThreadPoolExecutor(max_workers=min(8, len(repos))) as pool:
141
- per_repo = pool.map(lambda repo: _active_runs_for_repo(repo, github_config, jenkins_config), repos)
142
- pipelines = [item for group in per_repo for item in group]
143
- deduped_by_key: dict[tuple[str, str], ActivePipeline] = {}
144
- for pipeline in pipelines:
145
- deduped_by_key.setdefault(_pipeline_key(pipeline), pipeline)
146
- deduped = list(deduped_by_key.values())
147
- return sorted(deduped, key=lambda item: item.run.created_at or "", reverse=True)
165
+ per_repo = pool.map(lambda repo: _recent_runs_for_repo(repo, github_config, jenkins_config, limit), repos)
166
+ found = [item for group in per_repo for item in group]
167
+ deduped: dict[tuple[str, str], tuple[RepoSummary, CiRunInfo]] = {}
168
+ for repo, run in found:
169
+ deduped.setdefault(_run_key(run), (repo, run))
170
+ newest = sorted(deduped.values(), key=lambda item: _created_at_key(item[1]), reverse=True)[:limit]
171
+
172
+ def with_stages(item: tuple[RepoSummary, CiRunInfo]) -> ActivePipeline:
173
+ repo, run = item
174
+ if run.run_id:
175
+ run.stages = get_ci_run_stages(Path(repo.path), run.provider, run.run_id, github_config, jenkins_config)
176
+ return ActivePipeline(repo_id=repo.id, repo_name=repo.name, run=run)
177
+
178
+ if not newest:
179
+ return []
180
+ with ThreadPoolExecutor(max_workers=min(8, len(newest))) as pool:
181
+ return list(pool.map(with_stages, newest))
@@ -10,10 +10,12 @@ from urllib.request import Request, urlopen
10
10
 
11
11
  from git import Repo
12
12
 
13
+ from . import run_cache
13
14
  from .http import fetch_json
14
15
  from ..schemas import CiRunInfo, CiStage
15
16
 
16
17
 
18
+ PROVIDER = "github_actions"
17
19
  GITHUB_RUNS_PER_PAGE = 100
18
20
  GITHUB_RUNS_MAX_PAGES = 10
19
21
  ACTIVE_RUN_STATUSES = {"queued", "in_progress", "waiting", "pending", "requested"}
@@ -257,11 +259,47 @@ def _fetch_run_stages(github_config: dict, repo_config: dict, run_id: str) -> li
257
259
 
258
260
 
259
261
  def get_run_stages(repo_path: Path, github_config: dict | None, run_id: str) -> list[CiStage] | None:
262
+ """A run's jobs/steps; a finished run's come from the run cache once stored."""
260
263
  github_config = github_config or {}
261
264
  repo_config = _resolve_repo_config(github_config, repo_path)
262
265
  if repo_config is None:
263
266
  return None
264
- return _fetch_run_stages(github_config, repo_config, run_id)
267
+ cached = run_cache.get(PROVIDER, run_id)
268
+ if cached is not None and cached.run.stages is not None:
269
+ return cached.run.stages
270
+ stages = _fetch_run_stages(github_config, repo_config, run_id)
271
+ if stages is not None and cached is not None:
272
+ run_cache.put_stages(PROVIDER, run_id, stages)
273
+ return stages
274
+
275
+
276
+ def _remember_runs(scope: str, runs: list[tuple[CiRunInfo, set[str]]]) -> None:
277
+ """Store the finished runs among `runs` (with the commits they match). A
278
+ stored run that changed since (re-run: same id, new attempt) is replaced,
279
+ dropping its old stages."""
280
+ stored = run_cache.get_many(PROVIDER, [info.run_id for info, _ in runs if info.run_id])
281
+ for info, shas in runs:
282
+ cached = stored.get(info.run_id or "")
283
+ if cached is not None:
284
+ if cached.run.updated_at == info.updated_at and cached.run.status == info.status:
285
+ continue
286
+ run_cache.delete(PROVIDER, cached.run.run_id or "")
287
+ run_cache.put(PROVIDER, scope, info, shas)
288
+
289
+
290
+ def _unlisted_cached_runs(scope: str, listed: list[CiRunInfo], limit: int | None = None) -> list[run_cache.CachedRun]:
291
+ """Stored runs of the repo that the live listing didn't return, marked
292
+ `archived` when they fall inside the listed time range (so GitHub no longer
293
+ has them); older ones may just be past the capped listing."""
294
+ listed_ids = {info.run_id for info in listed}
295
+ oldest = min((info.created_at for info in listed if info.created_at), default=None)
296
+ result: list[run_cache.CachedRun] = []
297
+ for cached in run_cache.for_scope_prefixes(PROVIDER, [scope], limit):
298
+ if cached.run.run_id in listed_ids:
299
+ continue
300
+ cached.run.archived = oldest is not None and (cached.run.created_at or "") >= oldest
301
+ result.append(cached)
302
+ return result
265
303
 
266
304
 
267
305
  def get_active_runs(repo_path: Path, github_config: dict | None, head_sha: str | None = None) -> list[CiRunInfo]:
@@ -301,6 +339,22 @@ def get_active_runs(repo_path: Path, github_config: dict | None, head_sha: str |
301
339
  return result
302
340
 
303
341
 
342
+ def get_recent_runs(repo_path: Path, github_config: dict | None, limit: int) -> list[CiRunInfo]:
343
+ """The repo's latest runs, running or finished, newest first, without
344
+ stages. Same single cached call as `get_active_runs`."""
345
+ github_config = github_config or {}
346
+ repo_config = _resolve_repo_config(github_config, repo_path)
347
+ repo_url = _repo_api_url(github_config, repo_config) if repo_config else None
348
+ if repo_url is None:
349
+ return []
350
+ data = _get_json_cached(f"{repo_url}/actions/runs?{urlencode({'per_page': '30'})}", _headers(github_config))
351
+ items = data.get("workflow_runs") if isinstance(data, dict) else None
352
+ listed = [_run_info(run) for run in (items if isinstance(items, list) else []) if isinstance(run, dict)]
353
+ _remember_runs(repo_url, [(info, {info.head_sha} if info.head_sha else set()) for info in listed])
354
+ archived = [cached.run for cached in _unlisted_cached_runs(repo_url, listed, limit) if cached.run.archived]
355
+ return sorted(listed[:limit] + archived, key=lambda info: info.created_at or "", reverse=True)[:limit]
356
+
357
+
304
358
  def get_runs_by_id(repo_path: Path, github_config: dict | None, run_ids: list[str]) -> list[CiRunInfo]:
305
359
  """Current state of specific runs (one conditional call per run, plus one
306
360
  for the stages of a run that is still going). A run that has finished comes
@@ -393,12 +447,20 @@ def get_github_actions_runs(repo_path: Path, commit_hashes: set[str], github_con
393
447
 
394
448
  by_sha: dict[str, list[CiRunInfo]] = {}
395
449
  tags_by_name = _tag_targets(repo_path)
450
+ listed: list[tuple[CiRunInfo, set[str]]] = []
396
451
  for run in _fetch_workflow_runs(github_config, repo_config):
452
+ info = _run_info(run)
397
453
  sha = _matching_run_sha(run, commit_hashes, tags_by_name)
398
- if sha is None:
399
- continue
454
+ listed.append((info, {s for s in (info.head_sha, sha) if s}))
455
+ if sha is not None:
456
+ by_sha.setdefault(sha, []).append(info)
400
457
 
401
- info = _run_info(run)
402
- by_sha.setdefault(sha, []).append(info)
458
+ # Runs GitHub no longer lists (pruned past its retention) come from the cache.
459
+ repo_url = _repo_api_url(github_config, repo_config)
460
+ if repo_url is not None:
461
+ _remember_runs(repo_url, listed)
462
+ for cached in _unlisted_cached_runs(repo_url, [info for info, _ in listed]):
463
+ for sha in cached.shas & commit_hashes:
464
+ by_sha.setdefault(sha, []).append(cached.run.model_copy(update={"stages": None}))
403
465
 
404
466
  return by_sha