git-juggler 0.2.10__tar.gz → 0.2.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {git_juggler-0.2.10 → git_juggler-0.2.11}/PKG-INFO +1 -1
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/hook_events.py +201 -49
- git_juggler-0.2.11/src/backend/git_juggler/agents/hooks.py +529 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/git_resolver.py +78 -9
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/agents.py +2 -2
- git_juggler-0.2.11/src/backend/git_juggler/frontend_dist/assets/index-C-Yz67tX.js +23 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/index.html +1 -1
- git_juggler-0.2.11/src/backend/git_juggler/git/gitfiles.py +134 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/main.py +4 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/repo_discovery.py +3 -35
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/themes.py +12 -1
- git_juggler-0.2.11/src/backend/tests/hook_event_files.py +24 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_agent_hooks.py +283 -78
- git_juggler-0.2.11/src/backend/tests/test_agent_tracking.py +269 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_claude_sessions_data.py +9 -8
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_opencode_sessions.py +13 -13
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_themes.py +15 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Agents/AgentActivityPanel.tsx +3 -3
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Graph/GraphPanel.tsx +9 -11
- git_juggler-0.2.11/src/frontend/src/lib/agentRings.ts +75 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/storeCore.ts +27 -2
- git_juggler-0.2.11/src/frontend/tests/agentRings.test.ts +95 -0
- git_juggler-0.2.10/src/backend/git_juggler/agents/hooks.py +0 -305
- git_juggler-0.2.10/src/backend/git_juggler/frontend_dist/assets/index-DWGv6_yw.js +0 -22
- git_juggler-0.2.10/src/backend/tests/test_agent_tracking.py +0 -140
- {git_juggler-0.2.10 → git_juggler-0.2.11}/.github/workflows/build-package.yml +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/.gitignore +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/CLAUDE.md +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/LICENSE +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/README.md +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/notes.txt +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/pyproject.toml +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/resources/git-juggler-demo-1.gif +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/resources/git-juggler-demo-2.png +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/resources/git-juggler-demo-3.png +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/resources/git-juggler-demo-4.png +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/scripts/build-package.sh +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/scripts/dev.sh +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/__init__.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/__init__.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/__init__.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/activity_models.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/claude_sessions.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/claude_transcripts.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/opencode_sessions.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/paths.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/agents/tracking/text.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/__init__.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/ci.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/config.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/git.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/repos.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/api/terminal.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/app.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ci/__init__.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ci/github_actions.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ci/http.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ci/jenkins.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ci/run_cache.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/config.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/dev_app.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/agent-logos/claude.png +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/agent-logos/opencode.webp +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/assets/index-C6HbHDsB.css +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/ci-logos/jenkins.webp +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/git-juggler.png +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/frontend_dist/logo.png +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/__init__.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/command.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/data.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/detail.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/diff.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/refs.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/status.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/git/utils.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/schemas.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/terminal.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ui/__init__.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/git_juggler/ui/folder_picker.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/conftest.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_ci.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_ci_poll.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_ci_poll_route.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_file_diff.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_git_data.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_github_actions.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_jenkins.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_pick_folder.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_preferences.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_repos.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/backend/tests/test_run_cache.py +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/.gitignore +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/README.md +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/index.html +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/package-lock.json +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/package.json +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/public/agent-logos/claude.png +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/public/agent-logos/opencode.webp +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/public/ci-logos/jenkins.webp +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/public/git-juggler.png +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/public/logo.png +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/App.css +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/App.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/api/client.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/api/types.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Agents/AgentHoverCard.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Agents/AgentStatusPopover.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Agents/agentFormat.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Badges/BranchBadge.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Badges/CiRunBadge.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Badges/StashBadge.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Badges/TagBadge.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Badges/ciStatus.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Branches/DeleteBranchModal.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/CIRunPopover.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/CommandPalette/CommandPalette.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitDetail.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitList.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Commits/CommitRow.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Commits/LineCounts.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Commits/UncommittedRow.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/BranchContextMenu.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CommitContextMenu.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CreateBranchModal.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/CreateTagModal.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/RepoContextMenu.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/ContextMenu/TagContextMenu.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Diff/FileDiffModal.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Graph/branchColor.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Graph/computeColumns.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Graph/laneWidth.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/AgentQuickPicks.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/AgentsSettings.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/CiPollField.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/DirectoryBrowserModal.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/GitHubSettings.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/JenkinsSettings.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/MainMenu.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/RepoPathsSettings.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/ThemePicker.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Menu/ThemeQuickPicks.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Pipelines/PipelinesPanel.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Search/MultiSelect.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Search/SearchBox.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Search/TriSwitch.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/NewGroupModal.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/RepoList.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/Sidebar.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Sidebar/panelKit.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Tabs/TabsBar.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Tags/DeleteTagModal.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Terminal/TerminalPanel.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/Welcome/WelcomeWizard.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/icons.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/inputs/NumberField.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/inputs/TextField.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/inputs/ToggleField.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/components/inputs/styles.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/index.css +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/index.tsx +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/agentStatusPopoverData.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/appTheme.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/branchVisibility.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/bundledThemes.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/ciPoll.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/ciRunPopoverData.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/color.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/dismissOnOutsideClick.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/flip.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/formatDate.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/preferenceSender.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/scrollToCommit.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/sideBySide.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/stageWindow.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/stripAnsi.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/swallowNextClick.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/lib/vscodeTheme.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/agents.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/ci.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/graph.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/overlayStack.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/overlays.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/preferenceSync.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/preferences.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/repos.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/settings.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/store.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/tabs.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/terminal.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/state/themes.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/ci.css +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/commits.css +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/graph.css +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/menu.css +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/sidebar.css +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/src/styles/terminal.css +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/agentFormat.test.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/agentStatusPopoverData.test.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/ciPoll.test.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/ciRunPopoverData.test.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/computeColumns.test.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/preferenceSender.test.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/sideBySide.test.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/stageWindow.test.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tests/vscodeTheme.test.ts +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tsconfig.app.json +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tsconfig.json +0 -0
- {git_juggler-0.2.10 → git_juggler-0.2.11}/src/frontend/tsconfig.node.json +0 -0
- {git_juggler-0.2.10 → 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,8 +301,7 @@ 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
|
|
@@ -318,38 +473,35 @@ class AgentHookEventReader:
|
|
|
318
473
|
return (event.provider, f"cwd:{event.cwd or 'unknown'}")
|
|
319
474
|
|
|
320
475
|
def _read_events(self) -> list[HookEvent]:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
except OSError:
|
|
326
|
-
return []
|
|
327
|
-
events: list[HookEvent] = []
|
|
328
|
-
for line in lines:
|
|
329
|
-
try:
|
|
330
|
-
data = json.loads(line)
|
|
331
|
-
except json.JSONDecodeError:
|
|
332
|
-
continue
|
|
333
|
-
if not isinstance(data, dict):
|
|
334
|
-
continue
|
|
335
|
-
raw = data.get("raw") if isinstance(data.get("raw"), dict) else {}
|
|
336
|
-
timestamp = data.get("timestamp")
|
|
337
|
-
if not isinstance(timestamp, int):
|
|
338
|
-
continue
|
|
339
|
-
events.append(
|
|
340
|
-
HookEvent(
|
|
341
|
-
provider=str(data.get("provider") or "unknown"),
|
|
342
|
-
phase=str(data.get("phase") or "unknown"),
|
|
343
|
-
session_id=self._session_id(raw),
|
|
344
|
-
cwd=data.get("cwd") if isinstance(data.get("cwd"), str) else self._raw_string(raw, "cwd"),
|
|
345
|
-
pid=data.get("pid") if isinstance(data.get("pid"), int) else None,
|
|
346
|
-
agent_pid=self._agent_pid(data),
|
|
347
|
-
timestamp=timestamp,
|
|
348
|
-
raw=raw,
|
|
349
|
-
)
|
|
350
|
-
)
|
|
476
|
+
# The old log first: a session present in both has its older events there.
|
|
477
|
+
events = list(self.legacy_log.events(time.time()))
|
|
478
|
+
for file_events in self.session_files.events():
|
|
479
|
+
events.extend(file_events)
|
|
351
480
|
return events
|
|
352
481
|
|
|
482
|
+
def _parse_line(self, line: str, slim: bool = False) -> HookEvent | None:
|
|
483
|
+
try:
|
|
484
|
+
data = json.loads(line)
|
|
485
|
+
except json.JSONDecodeError:
|
|
486
|
+
return None
|
|
487
|
+
if not isinstance(data, dict):
|
|
488
|
+
return None
|
|
489
|
+
raw = data.get("raw") if isinstance(data.get("raw"), dict) else {}
|
|
490
|
+
timestamp = data.get("timestamp")
|
|
491
|
+
if not isinstance(timestamp, int):
|
|
492
|
+
return None
|
|
493
|
+
return HookEvent(
|
|
494
|
+
provider=str(data.get("provider") or "unknown"),
|
|
495
|
+
phase=str(data.get("phase") or "unknown"),
|
|
496
|
+
session_id=self._session_id(raw),
|
|
497
|
+
cwd=data.get("cwd") if isinstance(data.get("cwd"), str) else self._raw_string(raw, "cwd"),
|
|
498
|
+
pid=data.get("pid") if isinstance(data.get("pid"), int) else None,
|
|
499
|
+
agent_pid=self._agent_pid(data),
|
|
500
|
+
timestamp=timestamp,
|
|
501
|
+
# The old log kept whole payloads (tool output, file contents): keep only what is read.
|
|
502
|
+
raw=hooks.slim_raw(raw) if slim else raw,
|
|
503
|
+
)
|
|
504
|
+
|
|
353
505
|
def _candidate_paths(self, event: HookEvent) -> list[Path]:
|
|
354
506
|
paths: list[Path] = []
|
|
355
507
|
for raw_path in self._raw_paths(event.raw):
|