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.
- {git_juggler-0.2.9 → git_juggler-0.2.11}/PKG-INFO +1 -1
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/hook_events.py +210 -51
- git_juggler-0.2.11/src/backend/git_juggler/agents/hooks.py +529 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/activity_models.py +5 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/git_resolver.py +78 -9
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/opencode_sessions.py +6 -2
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/agents.py +7 -2
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/ci.py +14 -6
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/app.py +2 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ci/__init__.py +53 -19
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ci/github_actions.py +67 -5
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ci/jenkins.py +231 -23
- git_juggler-0.2.11/src/backend/git_juggler/ci/run_cache.py +214 -0
- git_juggler-0.2.11/src/backend/git_juggler/frontend_dist/assets/index-C-Yz67tX.js +23 -0
- git_juggler-0.2.11/src/backend/git_juggler/frontend_dist/assets/index-C6HbHDsB.css +1 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/index.html +2 -2
- git_juggler-0.2.11/src/backend/git_juggler/git/gitfiles.py +134 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/main.py +4 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/repo_discovery.py +3 -35
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/schemas.py +8 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/themes.py +12 -1
- git_juggler-0.2.11/src/backend/tests/conftest.py +16 -0
- git_juggler-0.2.11/src/backend/tests/hook_event_files.py +24 -0
- git_juggler-0.2.11/src/backend/tests/test_agent_hooks.py +706 -0
- git_juggler-0.2.11/src/backend/tests/test_agent_tracking.py +269 -0
- git_juggler-0.2.11/src/backend/tests/test_ci.py +113 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_claude_sessions_data.py +9 -8
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_github_actions.py +87 -1
- git_juggler-0.2.11/src/backend/tests/test_jenkins.py +554 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_opencode_sessions.py +13 -13
- git_juggler-0.2.11/src/backend/tests/test_run_cache.py +77 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_themes.py +15 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/api/client.ts +8 -2
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/api/types.ts +7 -0
- git_juggler-0.2.11/src/frontend/src/components/Agents/AgentActivityPanel.tsx +264 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Agents/AgentStatusPopover.tsx +9 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Agents/agentFormat.ts +29 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/CIRunPopover.tsx +20 -8
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Graph/GraphPanel.tsx +9 -11
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/MainMenu.tsx +25 -0
- git_juggler-0.2.11/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +308 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/RepoList.tsx +40 -4
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/Sidebar.tsx +2 -1
- git_juggler-0.2.11/src/frontend/src/components/Sidebar/panelKit.tsx +188 -0
- git_juggler-0.2.11/src/frontend/src/lib/agentRings.ts +75 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/agentStatusPopoverData.ts +2 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/ciRunPopoverData.ts +1 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/ci.ts +1 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/storeCore.ts +41 -6
- git_juggler-0.2.11/src/frontend/src/styles/ci.css +53 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/styles/sidebar.css +147 -217
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/agentFormat.test.ts +12 -1
- git_juggler-0.2.11/src/frontend/tests/agentRings.test.ts +95 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/agentStatusPopoverData.test.ts +11 -0
- git_juggler-0.2.9/src/backend/git_juggler/agents/hooks.py +0 -305
- git_juggler-0.2.9/src/backend/git_juggler/frontend_dist/assets/index-AgPjmxnQ.css +0 -1
- git_juggler-0.2.9/src/backend/git_juggler/frontend_dist/assets/index-DW1CPHRt.js +0 -22
- git_juggler-0.2.9/src/backend/tests/test_agent_hooks.py +0 -456
- git_juggler-0.2.9/src/backend/tests/test_agent_tracking.py +0 -140
- git_juggler-0.2.9/src/backend/tests/test_ci.py +0 -82
- git_juggler-0.2.9/src/backend/tests/test_jenkins.py +0 -275
- git_juggler-0.2.9/src/frontend/src/components/Agents/AgentActivityPanel.tsx +0 -85
- git_juggler-0.2.9/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +0 -81
- git_juggler-0.2.9/src/frontend/src/components/Pipelines/StageStrip.tsx +0 -64
- git_juggler-0.2.9/src/frontend/src/styles/ci.css +0 -174
- {git_juggler-0.2.9 → git_juggler-0.2.11}/.github/workflows/build-package.yml +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/.gitignore +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/CLAUDE.md +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/LICENSE +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/README.md +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/notes.txt +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/pyproject.toml +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/resources/git-juggler-demo-1.gif +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/resources/git-juggler-demo-2.png +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/resources/git-juggler-demo-3.png +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/resources/git-juggler-demo-4.png +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/scripts/build-package.sh +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/scripts/dev.sh +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/__init__.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/__init__.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/__init__.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/claude_sessions.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/claude_transcripts.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/paths.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/text.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/__init__.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/config.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/git.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/repos.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/api/terminal.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ci/http.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/config.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/dev_app.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/agent-logos/claude.png +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/agent-logos/opencode.webp +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/ci-logos/jenkins.webp +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/git-juggler.png +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/logo.png +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/__init__.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/command.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/data.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/detail.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/diff.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/refs.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/status.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/git/utils.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/terminal.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ui/__init__.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/git_juggler/ui/folder_picker.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_ci_poll.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_ci_poll_route.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_file_diff.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_git_data.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_pick_folder.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_preferences.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/backend/tests/test_repos.py +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/.gitignore +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/README.md +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/index.html +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/package-lock.json +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/package.json +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/public/agent-logos/claude.png +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/public/agent-logos/opencode.webp +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/public/ci-logos/jenkins.webp +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/public/git-juggler.png +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/public/logo.png +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/App.css +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/App.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Agents/AgentHoverCard.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Badges/BranchBadge.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Badges/CiRunBadge.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Badges/StashBadge.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Badges/TagBadge.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Badges/ciStatus.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Branches/DeleteBranchModal.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/CommandPalette/CommandPalette.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitDetail.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitList.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitRow.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Commits/LineCounts.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Commits/UncommittedRow.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/BranchContextMenu.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CommitContextMenu.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CreateBranchModal.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CreateTagModal.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/TagContextMenu.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Diff/FileDiffModal.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Graph/branchColor.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Graph/computeColumns.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Graph/laneWidth.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/AgentQuickPicks.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/AgentsSettings.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/CiPollField.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/DirectoryBrowserModal.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/GitHubSettings.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/JenkinsSettings.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/RepoPathsSettings.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/ThemePicker.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Menu/ThemeQuickPicks.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Search/MultiSelect.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Search/SearchBox.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Search/TriSwitch.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/NewGroupModal.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Tabs/TabsBar.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Tags/DeleteTagModal.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Terminal/TerminalPanel.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/Welcome/WelcomeWizard.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/icons.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/inputs/NumberField.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/inputs/TextField.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/inputs/ToggleField.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/components/inputs/styles.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/index.css +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/index.tsx +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/appTheme.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/branchVisibility.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/bundledThemes.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/ciPoll.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/color.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/dismissOnOutsideClick.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/flip.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/formatDate.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/preferenceSender.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/scrollToCommit.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/sideBySide.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/stageWindow.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/stripAnsi.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/swallowNextClick.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/lib/vscodeTheme.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/agents.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/graph.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/overlayStack.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/overlays.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/preferenceSync.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/preferences.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/repos.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/settings.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/store.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/tabs.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/terminal.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/state/themes.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/styles/commits.css +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/styles/graph.css +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/styles/menu.css +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/src/styles/terminal.css +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/ciPoll.test.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/ciRunPopoverData.test.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/computeColumns.test.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/preferenceSender.test.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/sideBySide.test.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/stageWindow.test.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tests/vscodeTheme.test.ts +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tsconfig.app.json +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tsconfig.json +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/tsconfig.node.json +0 -0
- {git_juggler-0.2.9 → git_juggler-0.2.11}/src/frontend/vite.config.ts +0 -0
|
@@ -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 .
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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):
|