git-juggler 0.2.15__tar.gz → 0.2.17__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.15 → git_juggler-0.2.17}/.gitignore +2 -1
  2. {git_juggler-0.2.15 → git_juggler-0.2.17}/PKG-INFO +32 -5
  3. {git_juggler-0.2.15 → git_juggler-0.2.17}/README.md +12 -4
  4. git_juggler-0.2.17/pyproject.toml +53 -0
  5. {git_juggler-0.2.15 → git_juggler-0.2.17}/scripts/dev.sh +4 -10
  6. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/hook_events.py +45 -0
  7. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/api/ci.py +2 -2
  8. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/api/config.py +22 -12
  9. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/api/git.py +24 -2
  10. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/api/repos.py +1 -1
  11. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/api/terminal.py +1 -1
  12. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/config.py +30 -2
  13. git_juggler-0.2.15/src/backend/git_juggler/frontend_dist/assets/index-CB9Jhv3C.js → git_juggler-0.2.17/src/backend/git_juggler/frontend_dist/assets/index-BOaJc15H.js +19 -16
  14. git_juggler-0.2.15/src/backend/git_juggler/frontend_dist/assets/index-0n6Gh9f_.css → git_juggler-0.2.17/src/backend/git_juggler/frontend_dist/assets/index-DRsp7rcC.css +1 -1
  15. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/frontend_dist/index.html +2 -2
  16. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/git/command.py +13 -2
  17. git_juggler-0.2.17/src/backend/git_juggler/git/conflicts.py +175 -0
  18. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/git/data.py +6 -1
  19. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/git/status.py +5 -1
  20. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/main.py +18 -2
  21. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/repo_discovery.py +39 -20
  22. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/schemas.py +39 -0
  23. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_ci_poll_route.py +1 -1
  24. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_claude_sessions_data.py +80 -0
  25. git_juggler-0.2.17/src/backend/tests/test_git_conflicts.py +77 -0
  26. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_preferences.py +33 -1
  27. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_repos.py +31 -1
  28. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/App.css +93 -0
  29. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/App.tsx +43 -11
  30. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/api/client.ts +10 -1
  31. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/api/types.ts +33 -0
  32. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/CommandPalette/CommandPalette.tsx +1 -0
  33. git_juggler-0.2.17/src/frontend/src/components/Conflicts/ConflictsPanel.tsx +267 -0
  34. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/ContextMenu/BranchContextMenu.tsx +16 -1
  35. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Menu/JenkinsSettings.tsx +33 -20
  36. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Menu/RepoPathsSettings.tsx +69 -44
  37. git_juggler-0.2.17/src/frontend/src/components/inputs/TemplateInput.tsx +236 -0
  38. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/agentRings.ts +4 -3
  39. git_juggler-0.2.17/src/frontend/src/lib/jenkinsUrlTemplate.ts +80 -0
  40. git_juggler-0.2.17/src/frontend/src/lib/templateTokens.ts +62 -0
  41. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/graph.ts +4 -0
  42. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/repos.ts +4 -0
  43. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/storeCore.ts +70 -1
  44. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/terminal.ts +1 -0
  45. git_juggler-0.2.17/src/frontend/src/styles/terminal.css +265 -0
  46. git_juggler-0.2.17/src/frontend/tests/templateTokens.test.ts +80 -0
  47. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/vite.config.ts +5 -2
  48. git_juggler-0.2.15/CLAUDE.md +0 -101
  49. git_juggler-0.2.15/pyproject.toml +0 -30
  50. git_juggler-0.2.15/src/frontend/src/styles/terminal.css +0 -64
  51. {git_juggler-0.2.15 → git_juggler-0.2.17}/.github/workflows/build-package.yml +0 -0
  52. {git_juggler-0.2.15 → git_juggler-0.2.17}/LICENSE +0 -0
  53. {git_juggler-0.2.15 → git_juggler-0.2.17}/notes.txt +0 -0
  54. {git_juggler-0.2.15 → git_juggler-0.2.17}/resources/git-juggler-demo-1.gif +0 -0
  55. {git_juggler-0.2.15 → git_juggler-0.2.17}/resources/git-juggler-demo-2.png +0 -0
  56. {git_juggler-0.2.15 → git_juggler-0.2.17}/resources/git-juggler-demo-3.png +0 -0
  57. {git_juggler-0.2.15 → git_juggler-0.2.17}/resources/git-juggler-demo-4.png +0 -0
  58. {git_juggler-0.2.15 → git_juggler-0.2.17}/scripts/build-package.sh +0 -0
  59. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/__init__.py +0 -0
  60. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/__init__.py +0 -0
  61. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/hooks.py +0 -0
  62. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/tracking/__init__.py +0 -0
  63. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/tracking/activity_models.py +0 -0
  64. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/tracking/claude_sessions.py +0 -0
  65. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/tracking/claude_transcripts.py +0 -0
  66. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/tracking/git_resolver.py +0 -0
  67. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/tracking/opencode_sessions.py +0 -0
  68. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/tracking/paths.py +0 -0
  69. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/agents/tracking/text.py +0 -0
  70. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/api/__init__.py +0 -0
  71. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/api/agents.py +0 -0
  72. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/app.py +0 -0
  73. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/ci/__init__.py +0 -0
  74. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/ci/github_actions.py +0 -0
  75. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/ci/http.py +0 -0
  76. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/ci/jenkins.py +0 -0
  77. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/ci/run_cache.py +0 -0
  78. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/dev_app.py +0 -0
  79. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/frontend_dist/agent-logos/claude.png +0 -0
  80. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/frontend_dist/agent-logos/opencode.webp +0 -0
  81. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/frontend_dist/ci-logos/jenkins.webp +0 -0
  82. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/frontend_dist/git-juggler.png +0 -0
  83. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/frontend_dist/logo.png +0 -0
  84. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/git/__init__.py +0 -0
  85. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/git/detail.py +0 -0
  86. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/git/diff.py +0 -0
  87. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/git/gitfiles.py +0 -0
  88. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/git/refs.py +0 -0
  89. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/git/utils.py +0 -0
  90. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/terminal.py +0 -0
  91. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/themes.py +0 -0
  92. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/ui/__init__.py +0 -0
  93. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/git_juggler/ui/folder_picker.py +0 -0
  94. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/conftest.py +0 -0
  95. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/hook_event_files.py +0 -0
  96. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_agent_hooks.py +0 -0
  97. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_agent_tracking.py +0 -0
  98. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_ci.py +0 -0
  99. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_ci_poll.py +0 -0
  100. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_commit_detail.py +0 -0
  101. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_file_diff.py +0 -0
  102. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_git_data.py +0 -0
  103. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_github_actions.py +0 -0
  104. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_jenkins.py +0 -0
  105. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_opencode_sessions.py +0 -0
  106. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_pick_folder.py +0 -0
  107. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_run_cache.py +0 -0
  108. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/backend/tests/test_themes.py +0 -0
  109. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/.gitignore +0 -0
  110. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/README.md +0 -0
  111. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/index.html +0 -0
  112. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/package-lock.json +0 -0
  113. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/package.json +0 -0
  114. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/public/agent-logos/claude.png +0 -0
  115. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/public/agent-logos/opencode.webp +0 -0
  116. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/public/ci-logos/jenkins.webp +0 -0
  117. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/public/git-juggler.png +0 -0
  118. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/public/logo.png +0 -0
  119. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Agents/AgentActivityPanel.tsx +0 -0
  120. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Agents/AgentHoverCard.tsx +0 -0
  121. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Agents/AgentStatusPopover.tsx +0 -0
  122. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Agents/agentFormat.ts +0 -0
  123. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Badges/BranchBadge.tsx +0 -0
  124. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Badges/CiRunBadge.tsx +0 -0
  125. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Badges/StashBadge.tsx +0 -0
  126. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Badges/TagBadge.tsx +0 -0
  127. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Badges/ciStatus.tsx +0 -0
  128. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Branches/DeleteBranchModal.tsx +0 -0
  129. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/CIRunPopover.tsx +0 -0
  130. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Commits/CommitDetail.tsx +0 -0
  131. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Commits/CommitList.tsx +0 -0
  132. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Commits/CommitRow.tsx +0 -0
  133. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Commits/LineCounts.tsx +0 -0
  134. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Commits/UncommittedRow.tsx +0 -0
  135. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/ContextMenu/CommitContextMenu.tsx +0 -0
  136. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/ContextMenu/CreateBranchModal.tsx +0 -0
  137. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/ContextMenu/CreateTagModal.tsx +0 -0
  138. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +0 -0
  139. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/ContextMenu/TagContextMenu.tsx +0 -0
  140. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Diff/FileDiffModal.tsx +0 -0
  141. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Graph/GraphPanel.tsx +0 -0
  142. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Graph/branchColor.ts +0 -0
  143. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Graph/computeColumns.ts +0 -0
  144. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Graph/laneWidth.ts +0 -0
  145. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/GraphToolbar/GraphToolbar.tsx +0 -0
  146. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Menu/AgentQuickPicks.tsx +0 -0
  147. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Menu/AgentsSettings.tsx +0 -0
  148. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Menu/CiPollField.tsx +0 -0
  149. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Menu/DirectoryBrowserModal.tsx +0 -0
  150. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Menu/GitHubSettings.tsx +0 -0
  151. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Menu/MainMenu.tsx +0 -0
  152. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Menu/ThemePicker.tsx +0 -0
  153. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Menu/ThemeQuickPicks.tsx +0 -0
  154. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +0 -0
  155. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Search/MultiSelect.tsx +0 -0
  156. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Search/SearchBox.tsx +0 -0
  157. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Sidebar/NewGroupModal.tsx +0 -0
  158. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Sidebar/RepoList.tsx +0 -0
  159. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Sidebar/Sidebar.tsx +0 -0
  160. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Sidebar/panelKit.tsx +0 -0
  161. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Tabs/TabsBar.tsx +0 -0
  162. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Tags/CreateTagDialog.tsx +0 -0
  163. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Tags/DeleteTagModal.tsx +0 -0
  164. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Terminal/TerminalPanel.tsx +0 -0
  165. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/Welcome/WelcomeWizard.tsx +0 -0
  166. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/icons.tsx +0 -0
  167. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/inputs/NumberField.tsx +0 -0
  168. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/inputs/TextField.tsx +0 -0
  169. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/inputs/ToggleField.tsx +0 -0
  170. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/components/inputs/styles.ts +0 -0
  171. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/index.css +0 -0
  172. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/index.tsx +0 -0
  173. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/agentStatusPopoverData.ts +0 -0
  174. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/appTheme.ts +0 -0
  175. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/branchVisibility.ts +0 -0
  176. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/bundledThemes.ts +0 -0
  177. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/ciPoll.ts +0 -0
  178. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/ciRunPopoverData.ts +0 -0
  179. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/color.ts +0 -0
  180. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/dismissOnOutsideClick.ts +0 -0
  181. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/flip.ts +0 -0
  182. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/formatDate.ts +0 -0
  183. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/preferenceSender.ts +0 -0
  184. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/scrollToCommit.ts +0 -0
  185. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/sideBySide.ts +0 -0
  186. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/stageWindow.ts +0 -0
  187. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/stripAnsi.ts +0 -0
  188. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/swallowNextClick.ts +0 -0
  189. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/lib/vscodeTheme.ts +0 -0
  190. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/agents.ts +0 -0
  191. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/ci.ts +0 -0
  192. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/overlayStack.ts +0 -0
  193. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/overlays.ts +0 -0
  194. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/preferenceSync.ts +0 -0
  195. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/preferences.ts +0 -0
  196. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/settings.ts +0 -0
  197. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/store.ts +0 -0
  198. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/tabs.ts +0 -0
  199. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/state/themes.ts +0 -0
  200. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/styles/ci.css +0 -0
  201. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/styles/commits.css +0 -0
  202. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/styles/graph.css +0 -0
  203. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/styles/menu.css +0 -0
  204. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/src/styles/sidebar.css +0 -0
  205. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tests/agentFormat.test.ts +0 -0
  206. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tests/agentRings.test.ts +0 -0
  207. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tests/agentStatusPopoverData.test.ts +0 -0
  208. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tests/ciPoll.test.ts +0 -0
  209. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tests/ciRunPopoverData.test.ts +0 -0
  210. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tests/computeColumns.test.ts +0 -0
  211. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tests/preferenceSender.test.ts +0 -0
  212. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tests/sideBySide.test.ts +0 -0
  213. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tests/stageWindow.test.ts +0 -0
  214. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tests/vscodeTheme.test.ts +0 -0
  215. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tsconfig.app.json +0 -0
  216. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tsconfig.json +0 -0
  217. {git_juggler-0.2.15 → git_juggler-0.2.17}/src/frontend/tsconfig.node.json +0 -0
@@ -25,7 +25,8 @@ secrets.env
25
25
  .vscode/
26
26
  *.sw?
27
27
 
28
- # Personal task notes (see CLAUDE.md)
28
+ # Local assistant/project notes
29
+ CLAUDE.md
29
30
  todo.txt
30
31
 
31
32
  plans/
@@ -1,9 +1,28 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: git-juggler
3
- Version: 0.2.15
3
+ Version: 0.2.17
4
4
  Summary: A local Git dashboard for people juggling agents and too many repos.
5
+ Project-URL: Homepage, https://github.com/franz38/git-juggler
6
+ Project-URL: Repository, https://github.com/franz38/git-juggler
7
+ Project-URL: Issues, https://github.com/franz38/git-juggler/issues
8
+ Project-URL: Releases, https://github.com/franz38/git-juggler/releases
5
9
  License-Expression: MIT
6
10
  License-File: LICENSE
11
+ Keywords: agents,dashboard,git,repositories,terminal
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: Microsoft :: Windows
19
+ Classifier: Operating System :: POSIX :: Linux
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Software Development :: Version Control :: Git
7
26
  Requires-Python: >=3.10
8
27
  Requires-Dist: fastapi>=0.110
9
28
  Requires-Dist: gitpython>=3.1
@@ -30,6 +49,14 @@ Watch agents work live, follow every repo’s git graph, and catch what changed
30
49
  - Connects GitHub Actions and Jenkins runs to commits and active pipelines.
31
50
  - VS Code-compatible themes.
32
51
 
52
+ ## Privacy / local-first
53
+
54
+ - Runs locally and binds to `127.0.0.1` by default.
55
+ - No telemetry or analytics are built in, and none are planned.
56
+ - Repo scanning and Claude/OpenCode session tracking read local files on your machine.
57
+ - Git mutations run through visible terminal commands, so you can see the exact command and output.
58
+ - Network requests are only used for features you configure, such as CI integrations.
59
+
33
60
  ## How to install
34
61
 
35
62
  git-juggler is published on PyPI. The recommended way to install it is with
@@ -46,7 +73,7 @@ to seed the initial search path; after that, search paths are managed in the app
46
73
  from the welcome wizard or Settings.
47
74
 
48
75
  - `--host HOST` changes the bind host from the default `127.0.0.1`.
49
- - `--port PORT` changes the backend port from the default `8000`.
76
+ - `--port PORT` changes the backend port from the default `8788`.
50
77
  - `--no-open` disables opening the app in your browser automatically.
51
78
 
52
79
  If you don't have pipx yet:
@@ -85,7 +112,7 @@ Use the dev script to start both the backend and frontend dev servers:
85
112
  ```
86
113
 
87
114
  - `path-to-scan` is optional. On first run, it seeds the folder whose immediate children are scanned for Git repos.
88
- - `--port PORT` is optional. It changes the backend port from the default `8000`; if you use it, update `src/frontend/vite.config.ts` locally so Vite proxies `/api` and `/ws` to the same port.
115
+ - `--port PORT` is optional. It changes the backend port from the default `8788`; the dev script passes the selected port to Vite automatically.
89
116
  - `--no-open` disables opening the app in your browser automatically.
90
117
 
91
118
  Or run them manually in two terminals:
@@ -99,7 +126,7 @@ python3 -m venv .venv
99
126
  ```
100
127
  cd src/frontend
101
128
  npm install
102
- npm run dev # http://localhost:5173 (proxies /api and /ws to :8000)
129
+ npm run dev # http://localhost:5173 (proxies /api and /ws to :8788)
103
130
  ```
104
131
 
105
132
  Open http://localhost:5173.
@@ -113,7 +140,7 @@ cd src/frontend && npm install && npm run build
113
140
  cd ../.. && .venv/bin/git-juggler <path-to-scan> [--no-open]
114
141
  ```
115
142
 
116
- The backend serves the built frontend in this mode, so only port 8000 is needed.
143
+ The backend serves the built frontend in this mode, so only port 8788 is needed.
117
144
  By default, it opens the app in your browser.
118
145
 
119
146
  ## Build a Python package
@@ -17,6 +17,14 @@ Watch agents work live, follow every repo’s git graph, and catch what changed
17
17
  - Connects GitHub Actions and Jenkins runs to commits and active pipelines.
18
18
  - VS Code-compatible themes.
19
19
 
20
+ ## Privacy / local-first
21
+
22
+ - Runs locally and binds to `127.0.0.1` by default.
23
+ - No telemetry or analytics are built in, and none are planned.
24
+ - Repo scanning and Claude/OpenCode session tracking read local files on your machine.
25
+ - Git mutations run through visible terminal commands, so you can see the exact command and output.
26
+ - Network requests are only used for features you configure, such as CI integrations.
27
+
20
28
  ## How to install
21
29
 
22
30
  git-juggler is published on PyPI. The recommended way to install it is with
@@ -33,7 +41,7 @@ to seed the initial search path; after that, search paths are managed in the app
33
41
  from the welcome wizard or Settings.
34
42
 
35
43
  - `--host HOST` changes the bind host from the default `127.0.0.1`.
36
- - `--port PORT` changes the backend port from the default `8000`.
44
+ - `--port PORT` changes the backend port from the default `8788`.
37
45
  - `--no-open` disables opening the app in your browser automatically.
38
46
 
39
47
  If you don't have pipx yet:
@@ -72,7 +80,7 @@ Use the dev script to start both the backend and frontend dev servers:
72
80
  ```
73
81
 
74
82
  - `path-to-scan` is optional. On first run, it seeds the folder whose immediate children are scanned for Git repos.
75
- - `--port PORT` is optional. It changes the backend port from the default `8000`; if you use it, update `src/frontend/vite.config.ts` locally so Vite proxies `/api` and `/ws` to the same port.
83
+ - `--port PORT` is optional. It changes the backend port from the default `8788`; the dev script passes the selected port to Vite automatically.
76
84
  - `--no-open` disables opening the app in your browser automatically.
77
85
 
78
86
  Or run them manually in two terminals:
@@ -86,7 +94,7 @@ python3 -m venv .venv
86
94
  ```
87
95
  cd src/frontend
88
96
  npm install
89
- npm run dev # http://localhost:5173 (proxies /api and /ws to :8000)
97
+ npm run dev # http://localhost:5173 (proxies /api and /ws to :8788)
90
98
  ```
91
99
 
92
100
  Open http://localhost:5173.
@@ -100,7 +108,7 @@ cd src/frontend && npm install && npm run build
100
108
  cd ../.. && .venv/bin/git-juggler <path-to-scan> [--no-open]
101
109
  ```
102
110
 
103
- The backend serves the built frontend in this mode, so only port 8000 is needed.
111
+ The backend serves the built frontend in this mode, so only port 8788 is needed.
104
112
  By default, it opens the app in your browser.
105
113
 
106
114
  ## Build a Python package
@@ -0,0 +1,53 @@
1
+ [project]
2
+ name = "git-juggler"
3
+ dynamic = ["version"]
4
+ description = "A local Git dashboard for people juggling agents and too many repos."
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ keywords = ["agents", "dashboard", "git", "repositories", "terminal"]
9
+ classifiers = [
10
+ "Development Status :: 4 - Beta",
11
+ "Environment :: Web Environment",
12
+ "Framework :: FastAPI",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: MIT License",
15
+ "Operating System :: MacOS",
16
+ "Operating System :: Microsoft :: Windows",
17
+ "Operating System :: POSIX :: Linux",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Topic :: Software Development :: Version Control :: Git",
24
+ ]
25
+ dependencies = [
26
+ "fastapi>=0.110",
27
+ "uvicorn[standard]>=0.29",
28
+ "gitpython>=3.1",
29
+ "pywinpty>=2.0; sys_platform == 'win32'",
30
+ ]
31
+
32
+ [project.scripts]
33
+ git-juggler = "git_juggler.main:cli"
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/franz38/git-juggler"
37
+ Repository = "https://github.com/franz38/git-juggler"
38
+ Issues = "https://github.com/franz38/git-juggler/issues"
39
+ Releases = "https://github.com/franz38/git-juggler/releases"
40
+
41
+ [build-system]
42
+ requires = ["hatchling", "hatch-vcs"]
43
+ build-backend = "hatchling.build"
44
+
45
+ [tool.hatch.version]
46
+ source = "vcs"
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/backend/git_juggler"]
50
+ artifacts = ["src/backend/git_juggler/frontend_dist/**"]
51
+
52
+ [tool.hatch.build.targets.sdist]
53
+ artifacts = ["src/backend/git_juggler/frontend_dist/**"]
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
  # Runs the backend and frontend dev servers together, both hot-reloading.
3
- # See CLAUDE.md's "How to run locally" section for the equivalent manual steps.
3
+ # See README.md's "Dev mode" section for the equivalent manual steps.
4
4
  set -euo pipefail
5
5
 
6
6
  ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
7
- BACKEND_PORT=8000
7
+ BACKEND_PORT=8788
8
8
  OPEN_BROWSER=1
9
9
 
10
10
  usage() {
@@ -12,8 +12,7 @@ usage() {
12
12
  echo " path-to-scan optional: directory whose first-level children get scanned for git repos." >&2
13
13
  echo " Only used to seed the config on first run; scan paths are otherwise" >&2
14
14
  echo " managed in the app (welcome wizard / Settings)." >&2
15
- echo " --port PORT backend port (default: 8000). If you change this, also update" >&2
16
- echo " src/frontend/vite.config.ts's proxy target -- don't commit that change." >&2
15
+ echo " --port PORT backend port (default: 8788)." >&2
17
16
  echo " --no-open don't open the app in a browser automatically." >&2
18
17
  }
19
18
 
@@ -115,7 +114,7 @@ fi
115
114
  BACKEND_PID=$!
116
115
 
117
116
  echo "Starting frontend dev server..."
118
- (cd "$ROOT_DIR/src/frontend" && npm run dev) &
117
+ (cd "$ROOT_DIR/src/frontend" && GIT_JUGGLER_BACKEND_URL="http://127.0.0.1:$BACKEND_PORT" npm run dev) &
119
118
  FRONTEND_PID=$!
120
119
 
121
120
  if [ "$OPEN_BROWSER" = "1" ]; then
@@ -125,11 +124,6 @@ fi
125
124
  echo ""
126
125
  echo "Backend: http://127.0.0.1:$BACKEND_PORT"
127
126
  echo "Frontend: http://localhost:5173 <- open this"
128
- if [ "$BACKEND_PORT" != "8000" ]; then
129
- echo ""
130
- echo "NOTE: backend is on a non-default port. Update the proxy target in" \
131
- "src/frontend/vite.config.ts to http://127.0.0.1:$BACKEND_PORT (local-only change, do not commit)."
132
- fi
133
127
  echo ""
134
128
  echo "Press Ctrl+C to stop both."
135
129
 
@@ -230,6 +230,8 @@ class AgentHookEventReader:
230
230
  self._cached_signature: tuple | None = None
231
231
  self._cached_scans: list[AgentRepositoryScan] = []
232
232
  self._cached_valid_until = 0
233
+ self._last_active_heads: dict[tuple[tuple[str, str], str], GitWorktreeInfo] = {}
234
+ self._idle_heads: dict[tuple[tuple[str, str], str], GitWorktreeInfo] = {}
233
235
 
234
236
  def recent_scans(self, now: int | None = None) -> list[AgentRepositoryScan]:
235
237
  """One scan per open agent session.
@@ -294,6 +296,7 @@ class AgentHookEventReader:
294
296
  opencode_signature = db_signature(self.opencode_db_path)
295
297
  scans: list[AgentRepositoryScan] = []
296
298
  next_change: int | None = None
299
+ seen_head_keys: set[tuple[tuple[str, str], str]] = set()
297
300
  for session_key, session_events in sessions.items():
298
301
  # A session is open until its end hook fires (or its process dies),
299
302
  # however long it has been quiet.
@@ -374,6 +377,8 @@ class AgentHookEventReader:
374
377
  elif busy is True:
375
378
  # Working right now: the most recently touched worktree is where.
376
379
  max(activities.values(), key=lambda item: item.last_activity).state = "active"
380
+ for activity in activities.values():
381
+ self._pin_idle_head(session_key, activity, seen_head_keys)
377
382
  details: SessionDetails | None = None
378
383
  if card is not None and session_id:
379
384
  details = self._session_details(card, session_id)
@@ -404,8 +409,48 @@ class AgentHookEventReader:
404
409
  parent_provider=provider if parent_opencode_session is not None else None,
405
410
  )
406
411
  )
412
+ self._prune_head_state(seen_head_keys)
407
413
  return scans, next_change
408
414
 
415
+ def _pin_idle_head(
416
+ self,
417
+ session_key: tuple[str, str],
418
+ activity: AgentWorktreeActivity,
419
+ seen: set[tuple[tuple[str, str], str]],
420
+ ) -> None:
421
+ key = (session_key, activity.worktree_path)
422
+ seen.add(key)
423
+ current = self._worktree_info(activity)
424
+ if activity.state == "active":
425
+ self._idle_heads.pop(key, None)
426
+ self._last_active_heads[key] = current
427
+ return
428
+
429
+ anchor = self._idle_heads.get(key)
430
+ if anchor is None or anchor.repository_id != activity.repository_id:
431
+ anchor = self._last_active_heads.get(key)
432
+ if anchor is None or anchor.repository_id != activity.repository_id:
433
+ anchor = current
434
+ self._idle_heads[key] = anchor
435
+ activity.branch = anchor.branch
436
+ activity.commit = anchor.commit
437
+
438
+ def _prune_head_state(self, seen: set[tuple[tuple[str, str], str]]) -> None:
439
+ for cache in (self._last_active_heads, self._idle_heads):
440
+ for key in list(cache):
441
+ if key not in seen:
442
+ del cache[key]
443
+
444
+ @staticmethod
445
+ def _worktree_info(activity: AgentWorktreeActivity) -> GitWorktreeInfo:
446
+ return GitWorktreeInfo(
447
+ repository_id=activity.repository_id,
448
+ worktree_path=activity.worktree_path,
449
+ common_git_dir=activity.repository_id,
450
+ branch=activity.branch,
451
+ commit=activity.commit,
452
+ )
453
+
409
454
  @staticmethod
410
455
  def _since_last_prompt(events: list[HookEvent]) -> list[HookEvent]:
411
456
  """The session's events from its latest user prompt onward.
@@ -25,7 +25,7 @@ router = APIRouter()
25
25
 
26
26
 
27
27
  def _resolve_repo_path(repo_id: str) -> Path:
28
- path = resolve_repo_path(config.load_repo_paths(), repo_id)
28
+ path = resolve_repo_path(config.load_repo_paths(), repo_id, config.load_individual_repo_paths())
29
29
  if path is None:
30
30
  raise HTTPException(status_code=404, detail="repo not found")
31
31
  return path
@@ -82,7 +82,7 @@ def api_ci_stages(repo_id: str, provider: str, run_id: str) -> list[CiStage]:
82
82
  @router.get("/api/ci/recent", response_model=list[ActivePipeline])
83
83
  def api_ci_recent(limit: Annotated[int, Query(ge=1, le=50)] = 10) -> list[ActivePipeline]:
84
84
  return get_recent_pipelines(
85
- list_repos(config.load_repo_paths()),
85
+ list_repos(config.load_repo_paths(), config.load_individual_repo_paths()),
86
86
  config.load_github_config(),
87
87
  config.load_jenkins_config(),
88
88
  limit,
@@ -25,6 +25,7 @@ def create_config_router(root_path: Path) -> APIRouter:
25
25
  def _current_config() -> ConfigResponse:
26
26
  return ConfigResponse(
27
27
  repo_paths=[str(p) for p in config.load_repo_paths()],
28
+ individual_repo_paths=[str(p) for p in config.load_individual_repo_paths()],
28
29
  pinned_repo_paths=config.load_pinned_repo_paths(),
29
30
  repo_groups=config.load_repo_groups(),
30
31
  excluded_paths=config.load_excluded_paths(),
@@ -32,27 +33,36 @@ def create_config_router(root_path: Path) -> APIRouter:
32
33
  github=config.load_github_config(),
33
34
  jenkins=config.load_jenkins_config(),
34
35
  terminal_shell=terminal_shell,
36
+ terminal_helper_command=[sys.executable, "-m", "git_juggler.main"],
35
37
  )
36
38
 
37
39
  @router.get("/api/config", response_model=ConfigResponse)
38
40
  def api_get_config() -> ConfigResponse:
39
41
  return _current_config()
40
42
 
43
+ def _resolve_dirs(raw_paths: list[str], *, require_git: bool) -> list[Path]:
44
+ resolved: list[Path] = []
45
+ seen: set[str] = set()
46
+ for raw in raw_paths:
47
+ path = Path(raw).expanduser().resolve()
48
+ if not path.is_dir():
49
+ raise HTTPException(status_code=400, detail=f"not a directory: {raw}")
50
+ if require_git and not (path / ".git").exists():
51
+ raise HTTPException(status_code=400, detail=f"not a git repository: {raw}")
52
+ key = str(path)
53
+ if key in seen:
54
+ continue
55
+ seen.add(key)
56
+ resolved.append(path)
57
+ return resolved
58
+
41
59
  @router.put("/api/config", response_model=ConfigResponse)
42
60
  def api_update_config(body: ConfigUpdateRequest) -> ConfigResponse:
43
61
  if body.repo_paths is not None:
44
- resolved: list[Path] = []
45
- seen: set[str] = set()
46
- for raw in body.repo_paths:
47
- path = Path(raw).expanduser().resolve()
48
- if not path.is_dir():
49
- raise HTTPException(status_code=400, detail=f"not a directory: {raw}")
50
- key = str(path)
51
- if key in seen:
52
- continue
53
- seen.add(key)
54
- resolved.append(path)
55
- config.save_repo_paths(resolved)
62
+ config.save_repo_paths(_resolve_dirs(body.repo_paths, require_git=False))
63
+
64
+ if body.individual_repo_paths is not None:
65
+ config.save_individual_repo_paths(_resolve_dirs(body.individual_repo_paths, require_git=True))
56
66
 
57
67
  if body.pinned_repo_paths is not None:
58
68
  deduped = list(dict.fromkeys(body.pinned_repo_paths))
@@ -5,18 +5,19 @@ from pathlib import Path
5
5
  from fastapi import APIRouter, HTTPException, Query
6
6
 
7
7
  from .. import config
8
+ from ..git.conflicts import create_resolution_token, get_conflict_file_content
8
9
  from ..git.data import GRAPH_PAGE_SIZE, HistoryChangedError, get_graph, get_repo_status
9
10
  from ..git.detail import get_commit_detail
10
11
  from ..git.diff import get_commit_file_diff, get_working_file_diff
11
12
  from ..repo_discovery import resolve_repo_path
12
- from ..schemas import CommitDetail, FileDiff, GraphResponse, RepoStatusResponse
13
+ from ..schemas import CommitDetail, ConflictFileContent, ConflictResolutionTokenRequest, ConflictResolutionTokenResponse, FileDiff, GraphResponse, RepoStatusResponse
13
14
 
14
15
 
15
16
  router = APIRouter()
16
17
 
17
18
 
18
19
  def _resolve_repo_path(repo_id: str) -> Path:
19
- path = resolve_repo_path(config.load_repo_paths(), repo_id)
20
+ path = resolve_repo_path(config.load_repo_paths(), repo_id, config.load_individual_repo_paths())
20
21
  if path is None:
21
22
  raise HTTPException(status_code=404, detail="repo not found")
22
23
  return path
@@ -75,3 +76,24 @@ def api_working_file_diff(
75
76
  return get_working_file_diff(repo_path, path, old_path, full)
76
77
  except Exception as exc: # noqa: BLE001 - surfaced as a 404 either way
77
78
  raise HTTPException(status_code=404, detail="diff not found") from exc
79
+
80
+
81
+ @router.get("/api/repos/{repo_id}/conflicts/content", response_model=ConflictFileContent)
82
+ def api_conflict_file_content(repo_id: str, path: str) -> ConflictFileContent:
83
+ repo_path = _resolve_repo_path(repo_id)
84
+ try:
85
+ return get_conflict_file_content(repo_path, path)
86
+ except Exception as exc: # noqa: BLE001 - surfaced as a 404 either way
87
+ raise HTTPException(status_code=404, detail="conflict file not found") from exc
88
+
89
+
90
+ @router.post("/api/repos/{repo_id}/conflicts/resolution-token", response_model=ConflictResolutionTokenResponse)
91
+ def api_create_conflict_resolution_token(
92
+ repo_id: str, request: ConflictResolutionTokenRequest
93
+ ) -> ConflictResolutionTokenResponse:
94
+ if request.repo_id != repo_id:
95
+ raise HTTPException(status_code=400, detail="repo mismatch")
96
+ _resolve_repo_path(repo_id)
97
+ return ConflictResolutionTokenResponse(
98
+ token=create_resolution_token(repo_id, request.path, request.content)
99
+ )
@@ -13,7 +13,7 @@ router = APIRouter()
13
13
 
14
14
  @router.get("/api/repos", response_model=list[RepoSummary])
15
15
  def api_list_repos() -> list[RepoSummary]:
16
- return list_repos(config.load_repo_paths())
16
+ return list_repos(config.load_repo_paths(), config.load_individual_repo_paths())
17
17
 
18
18
 
19
19
  @router.get("/api/repos/scan-progress", response_model=RepoScanProgress)
@@ -17,7 +17,7 @@ def create_terminal_router(root_path: Path) -> APIRouter:
17
17
  repo_id = websocket.query_params.get("repo")
18
18
  cwd = root_path
19
19
  if repo_id:
20
- resolved = resolve_repo_path(config.load_repo_paths(), repo_id)
20
+ resolved = resolve_repo_path(config.load_repo_paths(), repo_id, config.load_individual_repo_paths())
21
21
  if resolved is None:
22
22
  await websocket.close(code=1008)
23
23
  return
@@ -5,11 +5,22 @@ from pathlib import Path
5
5
 
6
6
  from pydantic import ValidationError
7
7
 
8
- from .schemas import Preferences
8
+ from .schemas import JenkinsConfig, JenkinsRuleConfig, Preferences
9
9
 
10
10
  CONFIG_DIR = Path.home() / ".config" / "git-juggler"
11
11
  CONFIG_PATH = CONFIG_DIR / "config.json"
12
12
 
13
+ # A fresh install ships one example Jenkins rule so the settings show how
14
+ # pipeline URL placeholders work. It has no repos, so nothing is ever requested
15
+ # from the placeholder host until the user assigns some. The settings' "Add
16
+ # rule" starts from the same rule (lib/jenkinsUrlTemplate.ts in the frontend).
17
+ EXAMPLE_JENKINS_BASE_URL = "https://jenkins.example.com"
18
+ EXAMPLE_JENKINS_RULE = JenkinsRuleConfig(
19
+ id="example",
20
+ name="Example rule",
21
+ job_url=EXAMPLE_JENKINS_BASE_URL + "/job/{repo_name_url}/job/{branch_name_url}",
22
+ )
23
+
13
24
 
14
25
  def _load_raw() -> dict:
15
26
  if not CONFIG_PATH.exists():
@@ -41,6 +52,21 @@ def save_repo_paths(paths: list[Path]) -> None:
41
52
  _save_raw(data)
42
53
 
43
54
 
55
+ def load_individual_repo_paths() -> list[Path]:
56
+ """Repos the user added one by one, on top of whatever the search paths
57
+ (``repo_paths``) turn up."""
58
+ raw = _load_raw().get("individual_repo_paths", [])
59
+ if not isinstance(raw, list):
60
+ return []
61
+ return [Path(p) for p in raw if isinstance(p, str)]
62
+
63
+
64
+ def save_individual_repo_paths(paths: list[Path]) -> None:
65
+ data = _load_raw()
66
+ data["individual_repo_paths"] = [str(p) for p in paths]
67
+ _save_raw(data)
68
+
69
+
44
70
  def load_pinned_repo_paths() -> list[str]:
45
71
  raw = _load_raw().get("pinned_repo_paths", [])
46
72
  if not isinstance(raw, list):
@@ -201,10 +227,12 @@ def save_jenkins_config(jenkins: dict | None) -> None:
201
227
 
202
228
  def ensure_seeded(default_path: Path) -> None:
203
229
  """On first run (no config file yet), seed it with the CLI-provided path
204
- so existing single-path usage keeps working without extra setup."""
230
+ so existing single-path usage keeps working without extra setup, plus the
231
+ example Jenkins rule."""
205
232
  if CONFIG_PATH.exists():
206
233
  return
207
234
  save_repo_paths([default_path])
235
+ save_jenkins_config(JenkinsConfig(base_url=EXAMPLE_JENKINS_BASE_URL, rules=[EXAMPLE_JENKINS_RULE]).model_dump())
208
236
 
209
237
 
210
238
  def reset_to_factory(default_path: Path) -> None: