├── logo.png
├── .env
├── src
├── vite-env.d.ts
├── assets
│ ├── logo.png
│ └── react.svg
├── setupTests.ts
├── components
│ ├── LoadingSpinner.tsx
│ ├── ErrorMessage.tsx
│ ├── ActivityFilter.tsx
│ ├── ActivityFilter.test.tsx
│ ├── ActivityList.tsx
│ ├── DateRangeFilter.tsx
│ ├── ActivityPieChart.tsx
│ ├── ActivityChart.tsx
│ ├── ActivityPieChart.test.tsx
│ ├── ActivityChart.test.tsx
│ ├── DateRangeFilter.test.tsx
│ └── ActivityList.test.tsx
├── types
│ ├── eslint-plugins.d.ts
│ └── index.ts
├── main.tsx
├── services
│ ├── github.ts
│ └── github.test.ts
├── index.css
├── test
│ ├── testUtils.ts
│ └── setup.ts
├── __integration_tests__
│ └── github.test.ts
├── App.tsx
└── App.css
├── public
├── logo.png
├── favicon.ico
└── cache
│ └── bot-activities.json
├── postcss.config.js
├── vercel.json
├── .vercel
└── project.json
├── .gitignore
├── vite.config.ts
├── scripts
├── tsconfig.json
├── package.json
└── src
│ ├── types.ts
│ ├── cache-github-data.ts
│ ├── github-api.test.ts
│ ├── monitor-cache.ts
│ └── github-api.ts
├── vitest.config.ts
├── index.html
├── .github
└── workflows
│ ├── test.yml
│ ├── lint.yml
│ ├── dependabot.yml
│ ├── ci.yml
│ └── openhands-resolver.yml
├── tsconfig.json
├── tsconfig.tsbuildinfo
├── api
├── status.js
└── cron.js
├── tsconfig.cache.json
├── tsconfig.node.json
├── tailwind.config.ts
├── LICENSE
├── tsconfig.app.json
├── eslint.config.ts
├── eslint.config.js
├── package.json
├── test
└── services
│ └── github-api.test.ts
└── README.md
/logo.png:
--------------------------------------------------------------------------------
1 | 404: Not Found
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | VITE_GITHUB_TOKEN=$GITHUB_TOKEN
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenHands/openhands-agent-monitor/HEAD/public/logo.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenHands/openhands-agent-monitor/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/OpenHands/openhands-agent-monitor/HEAD/src/assets/logo.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import '@testing-library/jest-dom';
3 | import './test/setup';
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "crons": [
3 | {
4 | "path": "/api/cron",
5 | "schedule": "0 */12 * * *"
6 | }
7 | ]
8 | }
--------------------------------------------------------------------------------
/.vercel/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "buildCommand": "npm run build:all",
3 | "devCommand": "npm run dev",
4 | "installCommand": "npm install"
5 | }
--------------------------------------------------------------------------------
/src/components/LoadingSpinner.tsx:
--------------------------------------------------------------------------------
1 | export function LoadingSpinner(): React.JSX.Element {
2 | return (
3 |
4 |
5 |
Loading activities...
6 |
7 | );
8 | }
--------------------------------------------------------------------------------
/src/types/eslint-plugins.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'eslint-plugin-react-hooks' {
2 | const plugin: any;
3 | export default plugin;
4 | }
5 |
6 | declare module 'eslint-plugin-react-refresh' {
7 | const plugin: any;
8 | export default plugin;
9 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vite.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | define: {
8 | 'process.env': JSON.stringify(process.env),
9 | },
10 | build: {
11 | outDir: 'dist',
12 | sourcemap: true,
13 | copyPublicDir: true,
14 | },
15 | });
16 |
--------------------------------------------------------------------------------
/scripts/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "module": "CommonJS",
5 | "moduleResolution": "node",
6 | "esModuleInterop": true,
7 | "strict": true,
8 | "outDir": "./dist",
9 | "rootDir": "./src",
10 | "skipLibCheck": true,
11 | "noEmit": false
12 | },
13 | "include": ["src/**/*.ts"],
14 | "exclude": ["node_modules"]
15 | }
--------------------------------------------------------------------------------
/src/components/ErrorMessage.tsx:
--------------------------------------------------------------------------------
1 | interface ErrorMessageProps {
2 | message: string;
3 | onRetry: () => void;
4 | }
5 |
6 | export function ErrorMessage({ message, onRetry }: ErrorMessageProps): React.JSX.Element {
7 | return (
8 |
9 |
{message}
10 |
11 |
12 | );
13 | }
--------------------------------------------------------------------------------
/src/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from 'react'
2 | import { createRoot } from 'react-dom/client'
3 | import './index.css'
4 | import App from './App.tsx'
5 |
6 | const rootElement = document.getElementById('root');
7 | if (rootElement === null) {
8 | throw new Error('Root element not found');
9 | }
10 |
11 | createRoot(rootElement).render(
12 |
13 |
14 | ,
15 | )
16 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | ///
2 | import { defineConfig } from 'vite';
3 | import react from '@vitejs/plugin-react';
4 |
5 | export default defineConfig({
6 | plugins: [react()],
7 | test: {
8 | globals: true,
9 | environment: 'jsdom',
10 | setupFiles: ['./src/setupTests.ts'],
11 | include: ['test/**/*.{test,spec}.{ts,tsx}', 'src/**/*.{test,spec}.{ts,tsx}'],
12 | },
13 | });
--------------------------------------------------------------------------------
/scripts/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "openhands-agent-monitor-scripts",
3 | "private": true,
4 | "version": "0.0.0",
5 | "scripts": {
6 | "test": "vitest"
7 | },
8 | "dependencies": {
9 | "node-fetch": "^2.6.7"
10 | },
11 | "devDependencies": {
12 | "@types/node": "^20.10.4",
13 | "@types/node-fetch": "^2.6.11",
14 | "typescript": "^5.3.3",
15 | "vitest": "^2.1.8"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | OpenHands Agent Activity Monitor
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Test
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | test:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Use Node.js 18.x
17 | uses: actions/setup-node@v3
18 | with:
19 | node-version: 18.x
20 | cache: 'npm'
21 |
22 | - name: Install dependencies
23 | run: npm ci
24 |
25 | - name: Run tests
26 | run: npm test
27 | env:
28 | CI: true
29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 | lint:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Use Node.js 18.x
17 | uses: actions/setup-node@v3
18 | with:
19 | node-version: 18.x
20 | cache: 'npm'
21 |
22 | - name: Install dependencies
23 | run: npm ci
24 |
25 | - name: Run ESLint
26 | run: npm run lint
27 |
28 | - name: Run TypeScript type checking
29 | run: npm run typecheck
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 | "moduleResolution": "bundler",
9 | "allowImportingTsExtensions": true,
10 | "noEmit": true,
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "react-jsx",
14 | "strict": true,
15 | "noUnusedLocals": true,
16 | "noUnusedParameters": true,
17 | "noFallthroughCasesInSwitch": true,
18 | "types": []
19 | },
20 | "include": ["src"],
21 | "exclude": ["node_modules"]
22 | }
23 |
--------------------------------------------------------------------------------
/tsconfig.tsbuildinfo:
--------------------------------------------------------------------------------
1 | {"root":["./src/App.tsx","./src/main.tsx","./src/setupTests.ts","./src/vite-env.d.ts","./src/__integration_tests__/github.test.ts","./src/components/ActivityChart.test.tsx","./src/components/ActivityChart.tsx","./src/components/ActivityFilter.test.tsx","./src/components/ActivityFilter.tsx","./src/components/ActivityList.test.tsx","./src/components/ActivityList.tsx","./src/components/DateRangeFilter.test.tsx","./src/components/DateRangeFilter.tsx","./src/components/ErrorMessage.tsx","./src/components/LoadingSpinner.tsx","./src/services/github.test.ts","./src/services/github.ts","./src/test/setup.ts","./src/test/testUtils.ts","./src/types/eslint-plugins.d.ts","./src/types/index.ts"],"version":"5.6.3"}
--------------------------------------------------------------------------------
/api/status.js:
--------------------------------------------------------------------------------
1 | export default async function handler(req, res) {
2 | if (req.method !== 'GET') {
3 | return res.status(405).json({ error: 'Method not allowed' });
4 | }
5 |
6 | try {
7 | const { readFile } = await import('fs/promises');
8 | const { join } = await import('path');
9 |
10 | const statusPath = join('public/cache/status.json');
11 | const status = JSON.parse(await readFile(statusPath, 'utf8'));
12 |
13 | return res.status(200).json(status);
14 | } catch (error) {
15 | console.error('Failed to read status:', error);
16 | return res.status(500).json({
17 | error: 'Failed to read cache status',
18 | details: error.message
19 | });
20 | }
21 | }
--------------------------------------------------------------------------------
/tsconfig.cache.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 | "moduleResolution": "node",
9 | "isolatedModules": true,
10 | "noEmit": false,
11 | "outDir": "./scripts",
12 | "jsx": "react-jsx",
13 | "strict": true,
14 | "noUnusedLocals": true,
15 | "noUnusedParameters": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "esModuleInterop": true,
18 | "allowJs": false,
19 | "checkJs": false
20 | },
21 | "include": ["scripts/github-api.ts", "src/types/index.ts"],
22 | "exclude": ["node_modules"]
23 | }
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "composite": true,
5 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
6 | "target": "ES2022",
7 | "lib": ["ES2023"],
8 | "module": "ESNext",
9 | "skipLibCheck": true,
10 |
11 | /* Bundler mode */
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "isolatedModules": true,
15 | "moduleDetection": "force",
16 |
17 | /* Linting */
18 | "strict": true,
19 | "noUnusedLocals": true,
20 | "noUnusedParameters": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "noUncheckedSideEffectImports": true,
23 | "types": ["vitest/globals"]
24 | },
25 | "include": ["vite.config.ts", "vitest.config.ts"]
26 | }
27 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot auto-merge
2 | on: pull_request
3 |
4 | permissions:
5 | contents: write
6 | pull-requests: write
7 |
8 | jobs:
9 | dependabot:
10 | runs-on: ubuntu-latest
11 | if: ${{ github.actor == 'dependabot[bot]' }}
12 | steps:
13 | - name: Dependabot metadata
14 | id: metadata
15 | uses: dependabot/fetch-metadata@v1
16 | with:
17 | github-token: "${{ secrets.GITHUB_TOKEN }}"
18 |
19 | - name: Enable auto-merge for Dependabot PRs
20 | if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
21 | run: gh pr merge --auto --merge "$PR_URL"
22 | env:
23 | PR_URL: ${{github.event.pull_request.html_url}}
24 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main, master ]
6 | pull_request:
7 | branches: [ main, master ]
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - uses: actions/checkout@v3
15 |
16 | - name: Use Node.js
17 | uses: actions/setup-node@v3
18 | with:
19 | node-version: '18.x'
20 | cache: 'npm'
21 |
22 | - name: Install dependencies
23 | run: npm ci
24 |
25 | - name: Type check
26 | run: npm run typecheck
27 | env:
28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29 |
30 | - name: Lint
31 | run: npm run lint
32 | env:
33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34 |
35 | - name: Build
36 | run: npm run build
37 |
38 | - name: Test
39 | run: npm run test
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/.github/workflows/openhands-resolver.yml:
--------------------------------------------------------------------------------
1 | name: Resolve Issue with OpenHands
2 |
3 | on:
4 | issues:
5 | types: [labeled]
6 | pull_request:
7 | types: [labeled]
8 | issue_comment:
9 | types: [created]
10 | pull_request_review_comment:
11 | types: [created]
12 | pull_request_review:
13 | types: [submitted]
14 |
15 | permissions:
16 | contents: write
17 | pull-requests: write
18 | issues: write
19 |
20 | jobs:
21 | call-openhands-resolver:
22 | uses: All-Hands-AI/OpenHands/.github/workflows/openhands-resolver.yml@main
23 | with:
24 | macro: ${{ vars.OPENHANDS_MACRO || '@openhands-agent' }}
25 | max_iterations: ${{ vars.OPENHANDS_MAX_ITER || 50 }}
26 | secrets:
27 | PAT_TOKEN: ${{ secrets.PAT_TOKEN }}
28 | PAT_USERNAME: ${{ secrets.PAT_USERNAME }}
29 | LLM_MODEL: ${{ secrets.LLM_MODEL }}
30 | LLM_API_KEY: ${{ secrets.LLM_API_KEY }}
31 | LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
32 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export type ActivityType = 'issue' | 'pr';
2 | export type PRStatus = 'no_pr' | 'pr_open' | 'pr_merged' | 'pr_closed';
3 | export type PRActivityStatus = 'success' | 'failure';
4 | export type IssueActivityStatus = PRStatus;
5 | export type ActivityStatus = PRActivityStatus | IssueActivityStatus;
6 |
7 | export interface BotActivity {
8 | id: string;
9 | type: ActivityType;
10 | status: ActivityStatus;
11 | timestamp: string;
12 | url: string;
13 | title: string;
14 | description: string;
15 | prUrl?: string;
16 | }
17 |
18 | export interface DateRange {
19 | start: string;
20 | end: string;
21 | }
22 |
23 | export interface ActivityFilter {
24 | type?: ActivityType | undefined;
25 | status?: ActivityStatus | undefined;
26 | dateRange?: DateRange | undefined;
27 | }
28 |
29 | export interface AppState {
30 | activities: BotActivity[];
31 | loading: boolean;
32 | error: string | null;
33 | filter: ActivityFilter;
34 | }
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'tailwindcss';
2 | import { nextui } from "@nextui-org/react";
3 | import typography from '@tailwindcss/typography';
4 |
5 | const config: Config = {
6 | content: [
7 | "./src/**/*.{js,ts,jsx,tsx}",
8 | "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
9 | ],
10 | theme: {
11 | extend: {
12 | colors: {
13 | 'root-primary': '#171717',
14 | 'root-secondary': '#262626',
15 | 'hyperlink': '#007AFF',
16 | 'danger': '#EF3744',
17 | },
18 | },
19 | },
20 | darkMode: "class",
21 | plugins: [
22 | nextui({
23 | defaultTheme: "dark",
24 | layout: {
25 | radius: {
26 | small: "5px",
27 | large: "20px",
28 | },
29 | },
30 | themes: {
31 | dark: {
32 | colors: {
33 | primary: "#4465DB",
34 | },
35 | }
36 | }
37 | }),
38 | typography,
39 | ],
40 | };
41 |
42 | export default config;
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 All-Hands-AI
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/scripts/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface GitHubComment {
2 | id: number;
3 | body: string;
4 | html_url: string;
5 | created_at: string;
6 | user: {
7 | login: string;
8 | type?: string;
9 | };
10 | }
11 |
12 | export interface GitHubIssue {
13 | number: number;
14 | title: string;
15 | html_url: string;
16 | comments_url: string;
17 | comments: number;
18 | pull_request?: unknown;
19 | body: string;
20 | }
21 |
22 | export interface GitHubPR {
23 | number: number;
24 | title: string;
25 | html_url: string;
26 | comments_url: string;
27 | comments: number;
28 | body?: string;
29 | }
30 |
31 | export interface GitHubPRResponse {
32 | state: string;
33 | merged: boolean;
34 | }
35 |
36 | export interface ApiResponse {
37 | data: T | T[];
38 | hasNextPage: boolean;
39 | nextUrl: string | null;
40 | }
41 |
42 | export type IssueStatus = 'no_pr' | 'pr_open' | 'pr_merged' | 'pr_closed';
43 | export type PRStatus = 'success' | 'failure';
44 |
45 | export interface Activity {
46 | id: string;
47 | type: 'issue' | 'pr';
48 | status: IssueStatus | PRStatus;
49 | timestamp: string;
50 | url: string;
51 | title: string;
52 | description: string;
53 | prUrl?: string;
54 | }
--------------------------------------------------------------------------------
/scripts/src/cache-github-data.ts:
--------------------------------------------------------------------------------
1 | import { fetchBotActivities } from './github-api';
2 | import fs from 'fs/promises';
3 | import path from 'path';
4 | import type { Activity } from './types';
5 |
6 | interface CacheData {
7 | activities: Activity[];
8 | lastUpdated: string;
9 | }
10 |
11 | async function cacheGitHubData(): Promise {
12 | try {
13 | // Fetch all activities for the last 30 days
14 | const activities = await fetchBotActivities();
15 |
16 | // Create cache directory if it doesn't exist
17 | const rootDir = path.resolve(process.cwd(), '..');
18 | const cacheDir = path.join(rootDir, 'public', 'cache');
19 | await fs.mkdir(cacheDir, { recursive: true });
20 |
21 | // Write activities to cache file
22 | const cacheFile = path.join(cacheDir, 'bot-activities.json');
23 | const cacheData: CacheData = {
24 | activities,
25 | lastUpdated: new Date().toISOString()
26 | };
27 |
28 | await fs.writeFile(cacheFile, JSON.stringify(cacheData, null, 2));
29 |
30 | console.log('Successfully cached GitHub data');
31 | } catch (error) {
32 | console.error('Error caching GitHub data:', error);
33 | process.exit(1);
34 | }
35 | }
36 |
37 | cacheGitHubData();
--------------------------------------------------------------------------------
/scripts/src/github-api.test.ts:
--------------------------------------------------------------------------------
1 | import { describe, it, expect } from 'vitest';
2 | import { isSuccessComment, isPRModificationFailureComment } from './github-api';
3 | import type { GitHubComment } from './types';
4 |
5 | describe('Comment detection', () => {
6 | it('should detect issue success comment', () => {
7 | const comment: GitHubComment = {
8 | id: 1,
9 | html_url: 'https://github.com/All-Hands-AI/OpenHands/issues/1#comment-1',
10 | created_at: '2023-11-28T00:01:00Z',
11 | user: { login: 'github-actions[bot]', type: 'Bot' },
12 | body: 'A potential fix has been generated and a draft PR #5364 has been created. Please review the changes.',
13 | };
14 | expect(isSuccessComment(comment)).toBe(true);
15 | });
16 |
17 | it('should detect PR failure comment', () => {
18 | const comment: GitHubComment = {
19 | id: 1,
20 | html_url: 'https://github.com/All-Hands-AI/OpenHands/issues/1#comment-1',
21 | created_at: '2023-11-28T00:01:00Z',
22 | user: { login: 'github-actions[bot]', type: 'Bot' },
23 | body: 'The workflow to fix this issue encountered an error. Please check the workflow logs for more information.',
24 | };
25 | expect(isPRModificationFailureComment(comment)).toBe(true);
26 | });
27 | });
--------------------------------------------------------------------------------
/src/services/github.ts:
--------------------------------------------------------------------------------
1 | import { BotActivity } from '../types';
2 |
3 | // PR status is now included in the cached data, no need to fetch it
4 |
5 | export async function fetchBotActivities(since?: string): Promise {
6 | try {
7 | const response = await fetch('/cache/bot-activities.json');
8 | if (!response.ok) {
9 | throw new Error(`Failed to load cached data: ${response.status.toString()} ${response.statusText}`);
10 | }
11 |
12 | const data = await response.json();
13 | let activities = data.activities as BotActivity[];
14 |
15 | // Filter by date if since is provided
16 | if (since !== undefined && since !== '') {
17 | const sinceDate = new Date(since).getTime();
18 | activities = activities.filter(activity =>
19 | new Date(activity.timestamp).getTime() >= sinceDate
20 | );
21 | }
22 |
23 | // PR status is already included in the cached data
24 | const processedActivities = activities;
25 |
26 | // Sort by timestamp in descending order
27 | return processedActivities.sort((a, b) =>
28 | new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
29 | );
30 | } catch (error) {
31 | console.error('Error fetching bot activities:', error);
32 | throw error;
33 | }
34 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | :root {
2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
3 | line-height: 1.5;
4 | font-weight: 400;
5 |
6 | color-scheme: light dark;
7 | color: rgba(255, 255, 255, 0.87);
8 | background-color: #242424;
9 |
10 | font-synthesis: none;
11 | text-rendering: optimizeLegibility;
12 | -webkit-font-smoothing: antialiased;
13 | -moz-osx-font-smoothing: grayscale;
14 | }
15 |
16 | a {
17 | font-weight: 500;
18 | color: #646cff;
19 | text-decoration: inherit;
20 | }
21 | a:hover {
22 | color: #535bf2;
23 | }
24 |
25 | body {
26 | margin: 0;
27 | display: flex;
28 | place-items: center;
29 | min-width: 320px;
30 | min-height: 100vh;
31 | }
32 |
33 | h1 {
34 | font-size: 3.2em;
35 | line-height: 1.1;
36 | }
37 |
38 | button {
39 | border-radius: 8px;
40 | border: 1px solid transparent;
41 | padding: 0.6em 1.2em;
42 | font-size: 1em;
43 | font-weight: 500;
44 | font-family: inherit;
45 | background-color: #1a1a1a;
46 | cursor: pointer;
47 | transition: border-color 0.25s;
48 | }
49 | button:hover {
50 | border-color: #646cff;
51 | }
52 | button:focus,
53 | button:focus-visible {
54 | outline: 4px auto -webkit-focus-ring-color;
55 | }
56 |
57 | @media (prefers-color-scheme: light) {
58 | :root {
59 | color: #213547;
60 | background-color: #ffffff;
61 | }
62 | a:hover {
63 | color: #747bff;
64 | }
65 | button {
66 | background-color: #f9f9f9;
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "composite": true,
5 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
6 | "target": "ES2020",
7 | "useDefineForClassFields": true,
8 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
9 | "module": "ESNext",
10 | "skipLibCheck": true,
11 |
12 | /* Bundler mode */
13 | "moduleResolution": "bundler",
14 | "allowImportingTsExtensions": true,
15 | "isolatedModules": true,
16 | "moduleDetection": "force",
17 | "jsx": "react-jsx",
18 |
19 | /* Linting */
20 | "strict": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "noUncheckedIndexedAccess": true,
25 | "noImplicitReturns": true,
26 | "noImplicitOverride": true,
27 | "noPropertyAccessFromIndexSignature": true,
28 | "exactOptionalPropertyTypes": true,
29 | "noUncheckedSideEffectImports": true,
30 |
31 | /* Type Checking */
32 | "allowUnreachableCode": false,
33 | "allowUnusedLabels": false,
34 | "noImplicitAny": true,
35 | "strictNullChecks": true,
36 | "strictFunctionTypes": true,
37 | "strictBindCallApply": true,
38 | "strictPropertyInitialization": true,
39 | "noImplicitThis": true,
40 | "useUnknownInCatchVariables": true,
41 | "alwaysStrict": true,
42 | "esModuleInterop": true,
43 | "allowJs": false,
44 | "checkJs": false,
45 | "types": ["vitest/globals"]
46 | },
47 | "include": ["src", "test", "*.ts"],
48 | "exclude": ["node_modules"]
49 | }
50 |
--------------------------------------------------------------------------------
/eslint.config.ts:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [
11 | js.configs.recommended,
12 | ...tseslint.configs.recommendedTypeChecked,
13 | ...tseslint.configs.strictTypeChecked,
14 | ],
15 | files: ['**/*.{ts,tsx}'],
16 | languageOptions: {
17 | ecmaVersion: 2020,
18 | globals: globals.browser,
19 | parserOptions: {
20 | project: ['./tsconfig.app.json', './tsconfig.node.json', './scripts/tsconfig.json'],
21 | tsconfigRootDir: import.meta.dirname,
22 | },
23 | },
24 | plugins: {
25 | 'react-hooks': reactHooks,
26 | 'react-refresh': reactRefresh,
27 | },
28 | rules: {
29 | ...reactHooks.configs.recommended.rules,
30 | 'react-refresh/only-export-components': [
31 | 'warn',
32 | { allowConstantExport: true },
33 | ],
34 | '@typescript-eslint/no-explicit-any': 'off',
35 | '@typescript-eslint/explicit-function-return-type': 'off',
36 | '@typescript-eslint/strict-boolean-expressions': 'off',
37 | '@typescript-eslint/no-unnecessary-condition': 'off',
38 | '@typescript-eslint/no-floating-promises': 'off',
39 | '@typescript-eslint/no-misused-promises': 'off',
40 | '@typescript-eslint/await-thenable': 'off',
41 | '@typescript-eslint/no-unsafe-assignment': 'off',
42 | '@typescript-eslint/no-unsafe-member-access': 'off',
43 | '@typescript-eslint/no-unsafe-call': 'off',
44 | '@typescript-eslint/no-unsafe-return': 'off',
45 | },
46 | },
47 | )
--------------------------------------------------------------------------------
/src/test/testUtils.ts:
--------------------------------------------------------------------------------
1 | export function getComputedStyle(element: HTMLElement, property: string): string {
2 | return window.getComputedStyle(element).getPropertyValue(property);
3 | }
4 |
5 | export function getCSSVariable(variableName: string): string {
6 | const style = getComputedStyle(document.documentElement, '--' + variableName);
7 | return style.trim();
8 | }
9 |
10 | // Helper to check if an element has dark theme compatible colors
11 | export function hasDarkThemeColors(element: HTMLElement): boolean {
12 | const bgColor = getComputedStyle(element, 'background-color');
13 | const color = getComputedStyle(element, 'color');
14 |
15 | // Convert color names and hex to rgb
16 | const colorToRgb = (color: string): number[] => {
17 | const div = document.createElement('div');
18 | div.style.color = color;
19 | document.body.appendChild(div);
20 | const computed = window.getComputedStyle(div).color;
21 | document.body.removeChild(div);
22 | return computed.match(/\d+/g)?.map(Number) || [];
23 | };
24 |
25 | // Convert rgb/rgba to brightness value (0-255)
26 | const getBrightness = (color: string): number => {
27 | const rgb = colorToRgb(color);
28 | if (rgb.length < 3) return 0;
29 | // Perceived brightness formula
30 | return (rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000;
31 | };
32 |
33 | const bgBrightness = getBrightness(bgColor);
34 | const textBrightness = getBrightness(color);
35 |
36 | // For dark theme:
37 | // - Background should be dark (brightness < 128)
38 | // - Text should be light (brightness > 128)
39 | // - There should be sufficient contrast between them
40 | return (
41 | bgBrightness < 128 &&
42 | textBrightness > 128 &&
43 | Math.abs(textBrightness - bgBrightness) > 50
44 | );
45 | }
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import js from '@eslint/js'
2 | import globals from 'globals'
3 | import reactHooks from 'eslint-plugin-react-hooks'
4 | import reactRefresh from 'eslint-plugin-react-refresh'
5 | import tseslint from 'typescript-eslint'
6 |
7 | export default tseslint.config(
8 | { ignores: ['dist'] },
9 | {
10 | extends: [
11 | js.configs.recommended,
12 | ...tseslint.configs.recommendedTypeChecked,
13 | ...tseslint.configs.strictTypeChecked,
14 | ],
15 | files: ['**/*.{ts,tsx}'],
16 | languageOptions: {
17 | ecmaVersion: 2020,
18 | globals: globals.browser,
19 | parserOptions: {
20 | project: ['./tsconfig.app.json', './tsconfig.node.json', './scripts/tsconfig.json', './tsconfig.json'],
21 | tsconfigRootDir: import.meta.dirname,
22 | },
23 | },
24 | plugins: {
25 | 'react-hooks': reactHooks,
26 | 'react-refresh': reactRefresh,
27 | },
28 | rules: {
29 | ...reactHooks.configs.recommended.rules,
30 | 'react-refresh/only-export-components': [
31 | 'warn',
32 | { allowConstantExport: true },
33 | ],
34 | '@typescript-eslint/no-explicit-any': 'off',
35 | '@typescript-eslint/explicit-function-return-type': 'off',
36 | '@typescript-eslint/strict-boolean-expressions': 'off',
37 | '@typescript-eslint/no-unnecessary-condition': 'off',
38 | '@typescript-eslint/no-floating-promises': 'off',
39 | '@typescript-eslint/no-misused-promises': 'off',
40 | '@typescript-eslint/await-thenable': 'off',
41 | '@typescript-eslint/no-unsafe-assignment': 'off',
42 | '@typescript-eslint/no-unsafe-member-access': 'off',
43 | '@typescript-eslint/no-unsafe-call': 'off',
44 | '@typescript-eslint/no-unsafe-return': 'off',
45 | },
46 | },
47 | )
--------------------------------------------------------------------------------
/src/components/ActivityFilter.tsx:
--------------------------------------------------------------------------------
1 | import { ActivityFilter as FilterType, ActivityStatus, ActivityType } from '../types';
2 |
3 | interface ActivityFilterProps {
4 | filter: FilterType;
5 | onFilterChange: (filter: FilterType) => void;
6 | }
7 |
8 | export function ActivityFilter({ filter, onFilterChange }: ActivityFilterProps): React.JSX.Element {
9 | const handleTypeChange = (type: ActivityType | ''): void => {
10 | onFilterChange({
11 | ...filter,
12 | type: type === '' ? undefined : type,
13 | });
14 | };
15 |
16 | const handleStatusChange = (status: ActivityStatus | ''): void => {
17 | onFilterChange({
18 | ...filter,
19 | status: status === '' ? undefined : status,
20 | });
21 | };
22 |
23 | const handleTypeSelect = (e: React.ChangeEvent): void => {
24 | handleTypeChange(e.target.value as ActivityType | '');
25 | };
26 |
27 | const handleStatusSelect = (e: React.ChangeEvent): void => {
28 | handleStatusChange(e.target.value as ActivityStatus | '');
29 | };
30 |
31 | return (
32 |
33 |
34 |
35 |
44 |
45 |
46 |
47 |
48 |
57 |
58 |
59 | );
60 | }
--------------------------------------------------------------------------------
/src/components/ActivityFilter.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent } from '@testing-library/react';
2 | import { describe, it, expect, vi, beforeEach } from 'vitest';
3 | import { ActivityFilter } from './ActivityFilter';
4 | import { ActivityFilter as FilterType } from '../types';
5 |
6 | describe('ActivityFilter', () => {
7 | const mockFilter: FilterType = {};
8 | const mockOnFilterChange = vi.fn();
9 |
10 | beforeEach(() => {
11 | mockOnFilterChange.mockClear();
12 | });
13 |
14 | it('renders filter options correctly', () => {
15 | render();
16 |
17 | expect(screen.getByLabelText('Type:')).toBeInTheDocument();
18 | expect(screen.getByLabelText('Status:')).toBeInTheDocument();
19 | });
20 |
21 | it('handles type filter changes', () => {
22 | render();
23 |
24 | const typeSelect = screen.getByLabelText('Type:');
25 | fireEvent.change(typeSelect, { target: { value: 'issue' } });
26 |
27 | expect(mockOnFilterChange).toHaveBeenCalledWith({
28 | ...mockFilter,
29 | type: 'issue',
30 | });
31 | });
32 |
33 | it('handles status filter changes', () => {
34 | render();
35 |
36 | const statusSelect = screen.getByLabelText('Status:');
37 | fireEvent.change(statusSelect, { target: { value: 'success' } });
38 |
39 | expect(mockOnFilterChange).toHaveBeenCalledWith({
40 | ...mockFilter,
41 | status: 'success',
42 | });
43 | });
44 |
45 | it('clears filters when selecting "All"', () => {
46 | const initialFilter: FilterType = {
47 | type: 'issue',
48 | status: 'success',
49 | };
50 |
51 | render();
52 |
53 | const typeSelect = screen.getByLabelText('Type:');
54 | fireEvent.change(typeSelect, { target: { value: '' } });
55 |
56 | expect(mockOnFilterChange).toHaveBeenCalledWith({
57 | ...initialFilter,
58 | type: undefined,
59 | });
60 | });
61 | });
--------------------------------------------------------------------------------
/scripts/src/monitor-cache.ts:
--------------------------------------------------------------------------------
1 | interface CacheStatus {
2 | lastSuccessfulUpdate?: string;
3 | lastAttempt?: string;
4 | status: 'success' | 'error';
5 | activitiesCount?: number;
6 | error?: string;
7 | }
8 |
9 | async function checkCacheStatus(): Promise {
10 | try {
11 | const fs = await import('fs/promises');
12 | const path = await import('path');
13 |
14 | const statusPath = path.join(process.cwd(), 'public/cache/status.json');
15 | const cacheDataPath = path.join(process.cwd(), 'public/cache/bot-activities.json');
16 |
17 | // Read status file
18 | const statusData = JSON.parse(await fs.readFile(statusPath, 'utf8')) as CacheStatus;
19 |
20 | // Read cache file
21 | const cacheData = JSON.parse(await fs.readFile(cacheDataPath, 'utf8'));
22 |
23 | // Check cache freshness
24 | const lastUpdate = new Date(statusData.lastSuccessfulUpdate || 0);
25 | const now = new Date();
26 | const hoursSinceUpdate = (now.getTime() - lastUpdate.getTime()) / (1000 * 60 * 60);
27 |
28 | console.log('Cache Status Report:');
29 | console.log('-------------------');
30 | console.log(`Status: ${statusData.status}`);
31 | console.log(`Last Successful Update: ${statusData.lastSuccessfulUpdate || 'Never'}`);
32 | console.log(`Hours Since Last Update: ${String(hoursSinceUpdate.toFixed(2))}`);
33 | console.log(`Activities in Cache: ${String(cacheData.activities?.length || 0)}`);
34 |
35 | if (statusData.error) {
36 | console.error('Last Error:', statusData.error);
37 | }
38 |
39 | // Alert if cache is stale
40 | if (hoursSinceUpdate > 12) {
41 | throw new Error(`Cache is stale! Last update was ${String(hoursSinceUpdate.toFixed(2))} hours ago`);
42 | }
43 |
44 | // Alert if no activities
45 | if (!cacheData.activities?.length) {
46 | throw new Error('Cache contains no activities!');
47 | }
48 |
49 | console.log('\nCache status is healthy ✓');
50 |
51 | } catch (error) {
52 | console.error('\nCache Health Check Failed!');
53 | console.error(error instanceof Error ? error.message : String(error));
54 | process.exit(1);
55 | }
56 | }
57 |
58 | // Run the check if this file is being run directly
59 | if (require.main === module) {
60 | checkCacheStatus();
61 | }
--------------------------------------------------------------------------------
/src/components/ActivityList.tsx:
--------------------------------------------------------------------------------
1 | import { BotActivity } from '../types';
2 | import { useState } from 'react';
3 |
4 | interface ActivityListProps {
5 | activities: BotActivity[];
6 | }
7 |
8 | export function ActivityList({ activities }: ActivityListProps): React.JSX.Element {
9 | const [currentPage, setCurrentPage] = useState(1);
10 | const itemsPerPage = 20;
11 | const totalPages = Math.ceil(activities.length / itemsPerPage);
12 |
13 | const startIndex = (currentPage - 1) * itemsPerPage;
14 | const endIndex = startIndex + itemsPerPage;
15 | const currentActivities = activities.slice(startIndex, endIndex);
16 |
17 | const handlePageChange = (page: number) => {
18 | setCurrentPage(page);
19 | };
20 |
21 | return (
22 |
23 |
24 | {currentActivities.map((activity) => (
25 |
26 |
27 | {activity.title}
28 |
29 |
{activity.description}
30 |
43 |
44 | ))}
45 |
46 | {totalPages > 1 && (
47 |
48 |
54 |
55 | Page {currentPage} of {totalPages}
56 |
57 |
63 |
64 | )}
65 |
66 | );
67 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "temp",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "tsc -b && vite build --mode production",
9 | "build:cache": "echo 'Building cache...' && cd scripts && echo 'Installing dependencies...' && npm install && echo 'Compiling TypeScript...' && npx tsc && echo 'Generating GitHub data cache...' && node dist/cache-github-data.js && echo 'Cache generation complete'",
10 | "build:all": "npm run build:cache && npm run build",
11 | "lint": "eslint .",
12 | "lint:fix": "eslint . --fix",
13 | "preview": "vite preview",
14 | "test": "vitest run --exclude '**/__integration_tests__/**'",
15 | "test:watch": "vitest --exclude '**/__integration_tests__/**'",
16 | "test:coverage": "vitest run --coverage --exclude '**/__integration_tests__/**'",
17 | "test:integration": "vitest run '**/__integration_tests__/**'",
18 | "typecheck": "tsc --noEmit",
19 | "cache-data": "node scripts/cache-github-data.js",
20 | "monitor:cache": "cd scripts && npx tsc && node dist/monitor-cache.js",
21 | "build:cache:safe": "npm run build:cache || (echo 'Cache build failed, checking status...' && npm run monitor:cache)"
22 | },
23 | "dependencies": {
24 | "@nextui-org/react": "^2.4.8",
25 | "@tailwindcss/typography": "^0.5.15",
26 | "autoprefixer": "^10.4.20",
27 | "postcss": "^8.4.49",
28 | "react": "^18.3.1",
29 | "react-dom": "^18.3.1",
30 | "tailwindcss": "^3.4.15"
31 | },
32 | "devDependencies": {
33 | "@eslint/js": "^9.15.0",
34 | "@testing-library/jest-dom": "^6.6.3",
35 | "@testing-library/react": "^16.0.1",
36 | "@types/node": "^22.10.1",
37 | "@types/node-fetch": "^2.6.12",
38 | "@types/react": "^18.3.12",
39 | "@types/react-dom": "^18.3.1",
40 | "@typescript-eslint/eslint-plugin": "^8.16.0",
41 | "@typescript-eslint/parser": "^8.16.0",
42 | "@vitejs/plugin-react": "^4.3.4",
43 | "@vitest/coverage-v8": "^2.1.8",
44 | "eslint": "^9.15.0",
45 | "eslint-plugin-react-hooks": "^5.0.0",
46 | "eslint-plugin-react-refresh": "^0.4.14",
47 | "globals": "^15.12.0",
48 | "jsdom": "^25.0.1",
49 | "react-vega": "^7.6.0",
50 | "ts-node": "^10.9.2",
51 | "typescript": "~5.6.2",
52 | "typescript-eslint": "^8.15.0",
53 | "vega-lite": "^5.21.0",
54 | "vite": "^6.0.1",
55 | "vitest": "^2.1.8"
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/components/DateRangeFilter.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import { DateRange } from '../types';
3 |
4 | interface DateRangeFilterProps {
5 | dateRange?: DateRange | undefined;
6 | onDateRangeChange: (dateRange?: DateRange) => void;
7 | }
8 |
9 | export function DateRangeFilter({ dateRange, onDateRangeChange }: DateRangeFilterProps): React.JSX.Element {
10 | const getDefaultDateRange = () => {
11 | const end = new Date();
12 | const start = new Date();
13 | start.setDate(start.getDate() - 7);
14 | return {
15 | start: start.toISOString().split('T')[0],
16 | end: end.toISOString().split('T')[0]
17 | };
18 | };
19 |
20 | const defaultRange = getDefaultDateRange();
21 | const [start, setStart] = useState(dateRange?.start ?? defaultRange.start);
22 | const [end, setEnd] = useState(dateRange?.end ?? defaultRange.end);
23 |
24 | useEffect(() => {
25 | if (dateRange === undefined) {
26 | const defaultRange = getDefaultDateRange();
27 | setStart(defaultRange.start);
28 | setEnd(defaultRange.end);
29 | onDateRangeChange(defaultRange);
30 | } else {
31 | setStart(dateRange.start);
32 | setEnd(dateRange.end);
33 | }
34 | }, [dateRange, onDateRangeChange]);
35 |
36 | const handleStartDateChange = (event: React.ChangeEvent): void => {
37 | const newStart = event.target.value;
38 | setStart(newStart);
39 |
40 | if (newStart === '' && end === '') {
41 | onDateRangeChange(undefined);
42 | } else {
43 | onDateRangeChange({ start: newStart, end });
44 | }
45 | };
46 |
47 | const handleEndDateChange = (event: React.ChangeEvent): void => {
48 | const newEnd = event.target.value;
49 | setEnd(newEnd);
50 |
51 | if (start === '' && newEnd === '') {
52 | onDateRangeChange(undefined);
53 | } else {
54 | onDateRangeChange({ start, end: newEnd });
55 | }
56 | };
57 |
58 | const maxDate = new Date().toISOString().split('T')[0];
59 |
60 | return (
61 |
84 | );
85 | }
--------------------------------------------------------------------------------
/src/test/setup.ts:
--------------------------------------------------------------------------------
1 | import { vi } from 'vitest';
2 |
3 | // Mock CSS Modules
4 | const cssModule = new Proxy(
5 | {},
6 | {
7 | get: () => 'mock-css-module'
8 | }
9 | );
10 |
11 | // Mock CSS files
12 | const cssFile = `
13 | :root {
14 | --bg-dark: #0c0e10;
15 | --bg-light: #292929;
16 | --bg-input: #393939;
17 | --bg-workspace: #1f2228;
18 | --border: #3c3c4a;
19 | --text-editor-base: #9099AC;
20 | --text-editor-active: #C4CBDA;
21 | --bg-editor-sidebar: #24272E;
22 | --bg-editor-active: #31343D;
23 | --border-editor-sidebar: #3C3C4A;
24 | --bg-neutral-muted: #afb8c133;
25 | }
26 |
27 | .filter-group select,
28 | .filter-group input {
29 | padding: 0.5rem;
30 | border: 1px solid var(--border);
31 | border-radius: 4px;
32 | font-size: 1rem;
33 | background-color: var(--bg-input) !important;
34 | color: var(--text-editor-active) !important;
35 | min-width: 120px;
36 | }
37 |
38 | .filter-group input[type="date"]::-webkit-calendar-picker-indicator {
39 | filter: invert(1);
40 | cursor: pointer;
41 | }
42 |
43 | .filter-group {
44 | display: flex;
45 | align-items: center;
46 | gap: 0.5rem;
47 | margin: 0.5rem;
48 | }
49 |
50 | .pagination {
51 | display: flex;
52 | justify-content: center;
53 | align-items: center;
54 | gap: 1rem;
55 | margin-top: 2rem;
56 | }
57 |
58 | .pagination button {
59 | padding: 0.5rem 1rem;
60 | background-color: var(--bg-input) !important;
61 | color: var(--text-editor-active) !important;
62 | border: 1px solid var(--border);
63 | border-radius: 4px;
64 | cursor: pointer;
65 | transition: background-color 0.2s;
66 | }
67 |
68 | .pagination button:disabled {
69 | opacity: 0.5;
70 | cursor: not-allowed;
71 | }
72 |
73 | .pagination button:not(:disabled):hover {
74 | background: var(--bg-editor-active);
75 | }
76 |
77 | .pagination .page-info {
78 | color: var(--text-editor-base) !important;
79 | padding: 0.5rem 1rem;
80 | }
81 | `;
82 |
83 | // Create a style element and append it to the document head
84 | const style = document.createElement('style');
85 | style.textContent = cssFile;
86 | document.head.appendChild(style);
87 |
88 | // Add pseudo-element styles
89 | const pseudoStyles = document.createElement('style');
90 | pseudoStyles.textContent = `
91 | .filter-group input[type="date"]::-webkit-calendar-picker-indicator {
92 | filter: invert(1);
93 | cursor: pointer;
94 | }
95 |
96 | .pagination button:not(:disabled):hover {
97 | background-color: var(--bg-editor-active) !important;
98 | }
99 | `;
100 | document.head.appendChild(pseudoStyles);
101 |
102 | // Mock CSS imports
103 | vi.mock('*.css', () => cssModule);
104 | vi.mock('*.scss', () => cssModule);
105 | vi.mock('*.sass', () => cssModule);
106 | vi.mock('*.less', () => cssModule);
--------------------------------------------------------------------------------
/src/components/ActivityPieChart.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { VegaLite } from 'react-vega';
3 |
4 | import { BotActivity, ActivityType } from '../types';
5 |
6 | interface ActivityPieChartProps {
7 | activities: BotActivity[];
8 | type: ActivityType;
9 | }
10 |
11 | interface ChartData {
12 | status: string;
13 | count: number;
14 | }
15 |
16 | type ChartSpec = {
17 | $schema: string;
18 | data: { values: ChartData[] };
19 | mark: { type: 'arc'; innerRadius: number };
20 | encoding: {
21 | theta: {
22 | field: 'count';
23 | type: 'quantitative';
24 | };
25 | color: {
26 | field: 'status';
27 | type: 'nominal';
28 | title: 'Status';
29 | scale?: {
30 | domain: string[];
31 | range: string[];
32 | };
33 | legend?: {
34 | labelColor: string;
35 | titleColor: string;
36 | };
37 | };
38 | };
39 | width: number;
40 | height: number;
41 | title: string | { text: string; color: string };
42 | background?: string;
43 | config?: {
44 | view: {
45 | stroke: string;
46 | };
47 | };
48 | }
49 |
50 | export function ActivityPieChart({ activities, type }: ActivityPieChartProps): React.JSX.Element {
51 | const chartData = useMemo((): ChartData[] => {
52 | const filteredActivities = activities.filter(a => a.type === type);
53 | const statusCounts = new Map();
54 |
55 | filteredActivities.forEach(activity => {
56 | const count = statusCounts.get(activity.status) || 0;
57 | statusCounts.set(activity.status, count + 1);
58 | });
59 |
60 | return Array.from(statusCounts.entries()).map(([status, count]) => ({
61 | status,
62 | count,
63 | }));
64 | }, [activities, type]);
65 |
66 | const spec: ChartSpec = {
67 | $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
68 | data: { values: chartData },
69 | mark: { type: 'arc', innerRadius: 50 },
70 | encoding: {
71 | theta: {
72 | field: 'count',
73 | type: 'quantitative'
74 | },
75 | color: {
76 | field: 'status',
77 | type: 'nominal',
78 | title: 'Status',
79 | scale: type === 'issue' ? {
80 | domain: ['no_pr', 'pr_open', 'pr_merged', 'pr_closed'],
81 | range: ['#ffffff', '#4caf50', '#9c27b0', '#f44336']
82 | } : {
83 | domain: ['success', 'failure'],
84 | range: ['#22c55e', '#ef4444']
85 | },
86 | legend: {
87 | labelColor: '#C4CBDA',
88 | titleColor: '#C4CBDA'
89 | }
90 | },
91 | },
92 | width: 300,
93 | height: 300,
94 | background: '#1f2228',
95 | title: {
96 | text: `Total ${type.toUpperCase()} Status Distribution`,
97 | color: '#C4CBDA'
98 | },
99 | config: {
100 | view: {
101 | stroke: 'transparent'
102 | }
103 | }
104 | };
105 |
106 | return ;
107 | }
--------------------------------------------------------------------------------
/api/cron.js:
--------------------------------------------------------------------------------
1 | import { fetchBotActivities } from '../scripts/src/github-api';
2 |
3 | export default async function handler(req, res) {
4 | console.log('Cron job started:', new Date().toISOString());
5 |
6 | try {
7 | if (req.method !== 'GET') {
8 | console.log('Invalid method:', req.method);
9 | return res.status(405).json({ error: 'Method not allowed' });
10 | }
11 |
12 | // Check authorization for GET requests
13 | const { authorization } = req.headers;
14 | if (authorization !== `Bearer ${process.env.CRON_SECRET}`) {
15 | console.warn('Unauthorized access attempt');
16 | return res.status(401).json({ error: 'Unauthorized' });
17 | }
18 |
19 | console.log('Fetching bot activities...');
20 | const activities = await fetchBotActivities();
21 |
22 | // Store the cache in public/cache directory
23 | const cacheData = {
24 | activities,
25 | lastUpdated: new Date().toISOString()
26 | };
27 |
28 | // Write to a status file to track last successful update
29 | const statusData = {
30 | lastSuccessfulUpdate: new Date().toISOString(),
31 | activitiesCount: activities.length,
32 | status: 'success'
33 | };
34 |
35 | try {
36 | const { writeFile } = await import('fs/promises');
37 | const { join } = await import('path');
38 |
39 | // Ensure directories exist
40 | const { mkdir } = await import('fs/promises');
41 | await mkdir('public/cache', { recursive: true });
42 |
43 | // Write cache and status files
44 | await writeFile(
45 | join('public/cache/bot-activities.json'),
46 | JSON.stringify(cacheData, null, 2)
47 | );
48 |
49 | await writeFile(
50 | join('public/cache/status.json'),
51 | JSON.stringify(statusData, null, 2)
52 | );
53 |
54 | console.log('Cache files written successfully');
55 | } catch (fsError) {
56 | console.error('Failed to write cache files:', fsError);
57 | throw new Error('Failed to write cache files: ' + fsError.message);
58 | }
59 |
60 | console.log('Cron job completed successfully');
61 | return res.status(200).json({
62 | success: true,
63 | timestamp: new Date().toISOString(),
64 | activitiesCount: activities.length
65 | });
66 | } catch (error) {
67 | console.error('Cron job failed:', error);
68 |
69 | // Write error status
70 | try {
71 | const { writeFile } = await import('fs/promises');
72 | const { join } = await import('path');
73 |
74 | await writeFile(
75 | join('public/cache/status.json'),
76 | JSON.stringify({
77 | lastAttempt: new Date().toISOString(),
78 | status: 'error',
79 | error: error.message
80 | }, null, 2)
81 | );
82 | } catch (fsError) {
83 | console.error('Failed to write error status:', fsError);
84 | }
85 |
86 | return res.status(500).json({
87 | error: error.message,
88 | timestamp: new Date().toISOString(),
89 | stack: process.env.NODE_ENV === 'development' ? error.stack : undefined
90 | });
91 | }
92 | }
--------------------------------------------------------------------------------
/test/services/github-api.test.ts:
--------------------------------------------------------------------------------
1 | import { isSuccessComment, isFailureComment } from '../../scripts/src/github-api';
2 | import type { GitHubComment } from '../../scripts/src/types';
3 |
4 | describe('GitHub API Comment Detection', () => {
5 | const createBotComment = (body: string, type: string = 'Bot'): GitHubComment => ({
6 | id: 1,
7 | body,
8 | html_url: 'https://github.com/example/comment',
9 | created_at: '2024-12-03T00:00:00Z',
10 | user: {
11 | login: 'github-actions[bot]',
12 | type
13 | }
14 | });
15 |
16 | describe('isSuccessComment', () => {
17 | it('should detect PR creation success comments', () => {
18 | const comment = createBotComment('A potential fix has been generated and a draft PR #5364 has been created. Please review the changes.');
19 | expect(isSuccessComment(comment)).toBe(true);
20 | });
21 |
22 | it('should detect changes made success comments', () => {
23 | const comment = createBotComment('OpenHands made the following changes to resolve the issues');
24 | expect(isSuccessComment(comment)).toBe(true);
25 | });
26 |
27 | it('should detect successfully fixed comments', () => {
28 | const comment = createBotComment('Successfully fixed the issue');
29 | expect(isSuccessComment(comment)).toBe(true);
30 | });
31 |
32 | it('should detect updated PR comments', () => {
33 | const comment = createBotComment('Updated pull request with the latest changes');
34 | expect(isSuccessComment(comment)).toBe(true);
35 | });
36 |
37 | it('should not detect non-success comments', () => {
38 | const comment = createBotComment('Started fixing the issue');
39 | expect(isSuccessComment(comment)).toBe(false);
40 | });
41 |
42 | it('should not detect non-bot comments', () => {
43 | const comment = createBotComment('A potential fix has been generated', 'User');
44 | expect(isSuccessComment(comment)).toBe(false);
45 | });
46 | });
47 |
48 | describe('isFailureComment', () => {
49 | it('should detect error comments', () => {
50 | const comment = createBotComment('The workflow to fix this issue encountered an error');
51 | expect(isFailureComment(comment)).toBe(true);
52 | });
53 |
54 | it('should detect failed to create changes comments', () => {
55 | const comment = createBotComment('OpenHands failed to create any code changes');
56 | expect(isFailureComment(comment)).toBe(true);
57 | });
58 |
59 | it('should detect unsuccessful fix comments', () => {
60 | const comment = createBotComment('An attempt was made to automatically fix this issue, but it was unsuccessful');
61 | expect(isFailureComment(comment)).toBe(true);
62 | });
63 |
64 | it('should not detect non-failure comments', () => {
65 | const comment = createBotComment('Started fixing the issue');
66 | expect(isFailureComment(comment)).toBe(false);
67 | });
68 |
69 | it('should not detect non-bot comments', () => {
70 | const comment = createBotComment('The workflow encountered an error', 'User');
71 | expect(isFailureComment(comment)).toBe(false);
72 | });
73 | });
74 | });
--------------------------------------------------------------------------------
/src/components/ActivityChart.tsx:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { VegaLite } from 'react-vega';
3 |
4 | import { BotActivity, ActivityType } from '../types';
5 |
6 | interface ActivityChartProps {
7 | activities: BotActivity[];
8 | type: ActivityType;
9 | }
10 |
11 | interface ChartData {
12 | date: string;
13 | status: string;
14 | }
15 |
16 | type ChartSpec = {
17 | $schema: string;
18 | data: { values: ChartData[] };
19 | mark: { type: 'bar' };
20 | encoding: {
21 | x: {
22 | field: 'date';
23 | type: 'temporal';
24 | title: 'Date';
25 | axis?: {
26 | labelColor: string;
27 | titleColor: string;
28 | gridColor: string;
29 | domainColor: string;
30 | };
31 | };
32 | y: {
33 | aggregate: 'count';
34 | type: 'quantitative';
35 | title: 'Count';
36 | axis?: {
37 | labelColor: string;
38 | titleColor: string;
39 | gridColor: string;
40 | domainColor: string;
41 | };
42 | };
43 | color: {
44 | field: 'status';
45 | type: 'nominal';
46 | title: 'Status';
47 | scale?: {
48 | domain: string[];
49 | range: string[];
50 | };
51 | legend?: {
52 | labelColor: string;
53 | titleColor: string;
54 | };
55 | };
56 | };
57 | width: number;
58 | height: number;
59 | title: string | { text: string; color: string };
60 | background?: string;
61 | config?: {
62 | view: {
63 | stroke: string;
64 | };
65 | };
66 | }
67 |
68 | export function ActivityChart({ activities, type }: ActivityChartProps): React.JSX.Element {
69 | const chartData = useMemo((): ChartData[] => {
70 | return activities
71 | .filter(a => a.type === type)
72 | .map(a => {
73 | const [date] = a.timestamp.split('T');
74 | if (date === undefined || date === '') {
75 | throw new Error('Invalid timestamp format');
76 | }
77 | return {
78 | date,
79 | status: a.status,
80 | };
81 | });
82 | }, [activities, type]);
83 |
84 | const spec: ChartSpec = {
85 | $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
86 | data: { values: chartData },
87 | mark: { type: 'bar' },
88 | encoding: {
89 | x: {
90 | field: 'date',
91 | type: 'temporal',
92 | title: 'Date',
93 | axis: {
94 | labelColor: '#C4CBDA',
95 | titleColor: '#C4CBDA',
96 | gridColor: '#3c3c4a',
97 | domainColor: '#3c3c4a'
98 | }
99 | },
100 | y: {
101 | aggregate: 'count',
102 | type: 'quantitative',
103 | title: 'Count',
104 | axis: {
105 | labelColor: '#C4CBDA',
106 | titleColor: '#C4CBDA',
107 | gridColor: '#3c3c4a',
108 | domainColor: '#3c3c4a'
109 | }
110 | },
111 | color: {
112 | field: 'status',
113 | type: 'nominal',
114 | title: 'Status',
115 | scale: type === 'issue' ? {
116 | domain: ['no_pr', 'pr_open', 'pr_merged', 'pr_closed'],
117 | range: ['#ffffff', '#4caf50', '#9c27b0', '#f44336'] // White, Green, Purple, Red
118 | } : {
119 | domain: ['success', 'failure'],
120 | range: ['#22c55e', '#ef4444'] // Green for success, Red for failure
121 | },
122 | legend: {
123 | labelColor: '#C4CBDA',
124 | titleColor: '#C4CBDA'
125 | }
126 | },
127 | },
128 | width: 400,
129 | height: 300,
130 | background: '#1f2228',
131 | title: {
132 | text: `${type.toUpperCase()} Activity Over Time`,
133 | color: '#C4CBDA'
134 | },
135 | config: {
136 | view: {
137 | stroke: 'transparent'
138 | }
139 | }
140 | };
141 |
142 | return ;
143 | }
--------------------------------------------------------------------------------
/src/assets/react.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/__integration_tests__/github.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Integration tests for the GitHub service.
3 | *
4 | * These tests interact with the real GitHub API and require authentication.
5 | * They are not meant to be run in CI, but rather locally or in a controlled environment.
6 | *
7 | * Prerequisites:
8 | * - A valid GitHub token with repo scope must be set in VITE_GITHUB_TOKEN environment variable
9 | * - The token must have access to the All-Hands-AI/OpenHands repository
10 | *
11 | * To run these tests:
12 | * 1. Create a .env file in the project root with:
13 | * VITE_GITHUB_TOKEN=your_github_token
14 | * 2. Run the integration tests:
15 | * npm run test:integration
16 | *
17 | * Note: These tests may take longer to run due to API rate limits and network latency.
18 | * They also depend on the actual state of the repository, so results may vary.
19 | */
20 |
21 | import { describe, it, expect, vi } from 'vitest';
22 | import { fetchBotActivities } from '../services/github';
23 | import type { BotActivity } from '../types';
24 | import * as fs from 'fs';
25 |
26 | describe('GitHub Service Integration Tests', () => {
27 | // Skip these tests if VITE_GITHUB_TOKEN is not set
28 | const runTest = import.meta.env['VITE_GITHUB_TOKEN'] !== undefined && import.meta.env['VITE_GITHUB_TOKEN'] !== '' ? it : it.skip;
29 |
30 | runTest('should fetch real bot activities from OpenHands repository', async () => {
31 | // Mock the fetch function to return the actual cached data
32 | const cachedData = JSON.parse(fs.readFileSync('/workspace/openhands-agent-monitor/public/cache/bot-activities.json', 'utf8')) as BotActivity[];
33 | const mockFetch = vi.fn().mockImplementation((url: string) => {
34 | if (url === '/cache/bot-activities.json') {
35 | return Promise.resolve({
36 | ok: true,
37 | status: 200,
38 | statusText: 'OK',
39 | json: () => Promise.resolve(cachedData),
40 | headers: new Headers()
41 | } as Response);
42 | }
43 | throw new Error(`Unexpected URL: ${url}`);
44 | });
45 | vi.stubGlobal('fetch', mockFetch as unknown as typeof fetch);
46 |
47 | const activities = await fetchBotActivities();
48 |
49 | // Verify we got some activities
50 | expect(activities.length).toBeGreaterThan(0);
51 |
52 | // Verify each activity has the required fields
53 | const expectedFields: Record = {
54 | id: expect.any(String),
55 | type: expect.stringMatching(/^(issue|pr)$/),
56 | status: expect.stringMatching(/^(success|failure)$/),
57 | timestamp: expect.any(String),
58 | url: expect.stringMatching(/^https:\/\/github\.com\//),
59 | title: expect.any(String),
60 | description: expect.any(String),
61 | prUrl: expect.any(String),
62 | };
63 |
64 | for (const activity of activities) {
65 | expect(activity).toMatchObject(expectedFields);
66 |
67 | // Verify the timestamp is a valid date
68 | expect(new Date(activity.timestamp).toString()).not.toBe('Invalid Date');
69 |
70 | // Verify the URL points to the correct repository
71 | expect(activity.url).toContain('All-Hands-AI/OpenHands');
72 | }
73 |
74 | // Log some stats to help with debugging
75 | const issueCount = activities.filter(a => a.type === 'issue').length;
76 | const prCount = activities.filter(a => a.type === 'pr').length;
77 | const successCount = activities.filter(a => a.status === 'success').length;
78 | const failureCount = activities.filter(a => a.status === 'failure').length;
79 |
80 | console.log(`Found ${activities.length.toString()} activities:`);
81 | console.log(`- Issues: ${issueCount.toString()}`);
82 | console.log(`- PRs: ${prCount.toString()}`);
83 | console.log(`- Successes: ${successCount.toString()}`);
84 | console.log(`- Failures: ${failureCount.toString()}`);
85 |
86 | // Print the first activity for manual verification
87 | if (activities.length > 0) {
88 | console.log('\nFirst activity:', JSON.stringify(activities[0], null, 2));
89 | }
90 | }, 30000); // Increase timeout to 30s for API calls
91 | });
92 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect, useCallback } from 'react';
2 | import { ActivityList } from './components/ActivityList';
3 | import { ActivityFilter } from './components/ActivityFilter';
4 | import { DateRangeFilter } from './components/DateRangeFilter';
5 | import { ActivityChart } from './components/ActivityChart';
6 | import { ActivityPieChart } from './components/ActivityPieChart';
7 | import { LoadingSpinner } from './components/LoadingSpinner';
8 | import { ErrorMessage } from './components/ErrorMessage';
9 | import { ActivityFilter as FilterType, DateRange, AppState } from './types';
10 | import { fetchBotActivities } from './services/github';
11 | import './App.css';
12 |
13 | function App(): React.JSX.Element {
14 | const [state, setState] = useState({
15 | activities: [],
16 | loading: true,
17 | error: null,
18 | filter: {}
19 | });
20 |
21 | const loadActivities = useCallback(async (): Promise => {
22 | setState(prev => ({ ...prev, loading: true, error: null }));
23 | try {
24 | const since = state.filter.dateRange?.start;
25 | const activities = await fetchBotActivities(since);
26 | setState(prev => ({ ...prev, activities, loading: false }));
27 | } catch (error) {
28 | setState(prev => ({
29 | ...prev,
30 | loading: false,
31 | error: error instanceof Error ? error.message : 'An error occurred while fetching activities',
32 | }));
33 | }
34 | }, [state.filter.dateRange?.start]);
35 |
36 | useEffect(() => {
37 | void loadActivities();
38 | }, [loadActivities]);
39 |
40 | const handleFilterChange = (filter: FilterType): void => {
41 | setState(prev => ({ ...prev, filter }));
42 | };
43 |
44 | const handleDateRangeChange = (dateRange?: DateRange): void => {
45 | setState(prev => ({
46 | ...prev,
47 | filter: {
48 | ...prev.filter,
49 | dateRange,
50 | },
51 | }));
52 | };
53 |
54 | const handleRetry = (): void => {
55 | void loadActivities();
56 | };
57 |
58 | const filteredActivities = state.activities.filter((activity) => {
59 | if (state.filter.type && activity.type !== state.filter.type) return false;
60 | if (state.filter.status && activity.status !== state.filter.status) return false;
61 | if (state.filter.dateRange && state.filter.dateRange.start && state.filter.dateRange.end) {
62 | const activityDate = new Date(activity.timestamp);
63 | const startDate = new Date(state.filter.dateRange.start);
64 | const endDate = new Date(state.filter.dateRange.end);
65 | if (activityDate < startDate || activityDate > endDate) return false;
66 | }
67 | return true;
68 | });
69 |
70 | return (
71 |
72 |
73 |

74 |
OpenHands Agent Activity Monitor
75 |
76 |
77 |
78 | Filters
79 |
83 |
87 |
88 |
89 | {state.loading ? (
90 |
91 | ) : state.error !== null ? (
92 |
96 | ) : (
97 | <>
98 |
99 | Activity Charts
100 |
104 |
108 |
109 |
110 |
111 | Activity List
112 |
113 |
114 | >
115 | )}
116 |
117 | );
118 | }
119 |
120 | export default App;
121 |
--------------------------------------------------------------------------------
/src/components/ActivityPieChart.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import { describe, it, expect, vi } from 'vitest';
3 | import { ActivityPieChart } from './ActivityPieChart';
4 | import { BotActivity } from '../types';
5 |
6 | // Mock VegaLite component and capture the spec prop
7 | interface VegaLiteProps {
8 | spec: {
9 | data: { values: any[] };
10 | encoding: {
11 | theta: { field: string; type: string };
12 | color: { scale: { domain: string[]; range: string[] } };
13 | };
14 | title: { text: string; color: string };
15 | };
16 | }
17 |
18 | const mockVegaLite = vi.fn();
19 | vi.mock('react-vega', () => ({
20 | VegaLite: (props: VegaLiteProps) => {
21 | mockVegaLite(props);
22 | return ;
23 | },
24 | }));
25 |
26 | function getLastVegaLiteProps(): VegaLiteProps {
27 | expect(mockVegaLite).toHaveBeenCalled();
28 | const lastCall = mockVegaLite.mock.calls[mockVegaLite.mock.calls.length - 1][0];
29 | expect(lastCall).toBeDefined();
30 | return lastCall;
31 | }
32 |
33 | describe('ActivityPieChart', () => {
34 | const mockActivities: BotActivity[] = [
35 | {
36 | id: '1',
37 | type: 'issue',
38 | status: 'no_pr',
39 | timestamp: '2023-11-28T12:00:00Z',
40 | url: 'https://github.com/example/1',
41 | title: 'Test Issue 1',
42 | description: 'Issue without PR',
43 | },
44 | {
45 | id: '2',
46 | type: 'issue',
47 | status: 'pr_open',
48 | timestamp: '2023-11-28T13:00:00Z',
49 | url: 'https://github.com/example/2',
50 | title: 'Test Issue 2',
51 | description: 'Issue with open PR',
52 | prUrl: 'https://github.com/example/pr/2',
53 | },
54 | {
55 | id: '3',
56 | type: 'issue',
57 | status: 'pr_merged',
58 | timestamp: '2023-11-28T14:00:00Z',
59 | url: 'https://github.com/example/3',
60 | title: 'Test Issue 3',
61 | description: 'Issue with merged PR',
62 | prUrl: 'https://github.com/example/pr/3',
63 | },
64 | {
65 | id: '4',
66 | type: 'issue',
67 | status: 'pr_closed',
68 | timestamp: '2023-11-28T15:00:00Z',
69 | url: 'https://github.com/example/4',
70 | title: 'Test Issue 4',
71 | description: 'Issue with closed PR',
72 | prUrl: 'https://github.com/example/pr/4',
73 | },
74 | {
75 | id: '5',
76 | type: 'pr',
77 | status: 'success',
78 | timestamp: '2023-11-28T16:00:00Z',
79 | url: 'https://github.com/example/5',
80 | title: 'Test PR 1',
81 | description: 'Successful PR',
82 | },
83 | {
84 | id: '6',
85 | type: 'pr',
86 | status: 'failure',
87 | timestamp: '2023-11-28T17:00:00Z',
88 | url: 'https://github.com/example/6',
89 | title: 'Test PR 2',
90 | description: 'Failed PR',
91 | },
92 | ];
93 |
94 | it('renders pie chart component', () => {
95 | const { getByTestId } = render(
96 |
97 | );
98 |
99 | expect(getByTestId('vega-lite-pie-chart')).toBeInTheDocument();
100 | });
101 |
102 | it('aggregates data correctly for issues', () => {
103 | render();
104 | const lastCall = getLastVegaLiteProps();
105 | const chartData = lastCall.spec.data.values;
106 |
107 | expect(chartData).toHaveLength(4);
108 | expect(chartData).toEqual(expect.arrayContaining([
109 | { status: 'no_pr', count: 1 },
110 | { status: 'pr_open', count: 1 },
111 | { status: 'pr_merged', count: 1 },
112 | { status: 'pr_closed', count: 1 }
113 | ]));
114 | });
115 |
116 | it('aggregates data correctly for PRs', () => {
117 | render();
118 | const lastCall = getLastVegaLiteProps();
119 | const chartData = lastCall.spec.data.values;
120 |
121 | expect(chartData).toHaveLength(2);
122 | expect(chartData).toEqual(expect.arrayContaining([
123 | { status: 'success', count: 1 },
124 | { status: 'failure', count: 1 }
125 | ]));
126 | });
127 |
128 | it('configures issue color scale correctly', () => {
129 | render();
130 | const lastCall = getLastVegaLiteProps();
131 | const colorScale = lastCall.spec.encoding.color.scale;
132 |
133 | expect(colorScale.domain).toEqual(['no_pr', 'pr_open', 'pr_merged', 'pr_closed']);
134 | expect(colorScale.range).toEqual(['#ffffff', '#4caf50', '#9c27b0', '#f44336']);
135 | });
136 |
137 | it('configures PR color scale correctly', () => {
138 | render();
139 | const lastCall = getLastVegaLiteProps();
140 | const colorScale = lastCall.spec.encoding.color.scale;
141 |
142 | expect(colorScale.domain).toEqual(['success', 'failure']);
143 | expect(colorScale.range).toEqual(['#22c55e', '#ef4444']);
144 | });
145 |
146 | it('configures chart title correctly', () => {
147 | render();
148 | const lastCall = getLastVegaLiteProps();
149 | const { title } = lastCall.spec;
150 |
151 | expect(title).toEqual({
152 | text: 'Total ISSUE Status Distribution',
153 | color: '#C4CBDA'
154 | });
155 | });
156 | });
--------------------------------------------------------------------------------
/src/components/ActivityChart.test.tsx:
--------------------------------------------------------------------------------
1 | import { render } from '@testing-library/react';
2 | import { describe, it, expect, vi } from 'vitest';
3 | import { ActivityChart } from './ActivityChart';
4 | import { BotActivity } from '../types';
5 |
6 | // Mock VegaLite component and capture the spec prop
7 | interface VegaLiteProps {
8 | spec: {
9 | data: { values: any[] };
10 | encoding: {
11 | color: { scale: { domain: string[]; range: string[] } };
12 | x: any;
13 | y: any;
14 | };
15 | title: { text: string; color: string };
16 | };
17 | }
18 |
19 | const mockVegaLite = vi.fn();
20 | vi.mock('react-vega', () => ({
21 | VegaLite: (props: VegaLiteProps) => {
22 | mockVegaLite(props);
23 | return ;
24 | },
25 | }));
26 |
27 | function getLastVegaLiteProps(): VegaLiteProps {
28 | expect(mockVegaLite).toHaveBeenCalled();
29 | const lastCall = mockVegaLite.mock.calls[mockVegaLite.mock.calls.length - 1][0];
30 | expect(lastCall).toBeDefined();
31 | return lastCall;
32 | }
33 |
34 | describe('ActivityChart', () => {
35 | const mockActivities: BotActivity[] = [
36 | {
37 | id: '1',
38 | type: 'issue',
39 | status: 'no_pr',
40 | timestamp: '2023-11-28T12:00:00Z',
41 | url: 'https://github.com/example/1',
42 | title: 'Test Issue 1',
43 | description: 'Issue without PR',
44 | },
45 | {
46 | id: '2',
47 | type: 'issue',
48 | status: 'pr_open',
49 | timestamp: '2023-11-28T13:00:00Z',
50 | url: 'https://github.com/example/2',
51 | title: 'Test Issue 2',
52 | description: 'Issue with open PR',
53 | prUrl: 'https://github.com/example/pr/2',
54 | },
55 | {
56 | id: '3',
57 | type: 'issue',
58 | status: 'pr_merged',
59 | timestamp: '2023-11-28T14:00:00Z',
60 | url: 'https://github.com/example/3',
61 | title: 'Test Issue 3',
62 | description: 'Issue with merged PR',
63 | prUrl: 'https://github.com/example/pr/3',
64 | },
65 | {
66 | id: '4',
67 | type: 'issue',
68 | status: 'pr_closed',
69 | timestamp: '2023-11-28T15:00:00Z',
70 | url: 'https://github.com/example/4',
71 | title: 'Test Issue 4',
72 | description: 'Issue with closed PR',
73 | prUrl: 'https://github.com/example/pr/4',
74 | },
75 | {
76 | id: '5',
77 | type: 'pr',
78 | status: 'success',
79 | timestamp: '2023-11-28T16:00:00Z',
80 | url: 'https://github.com/example/5',
81 | title: 'Test PR 1',
82 | description: 'Successful PR',
83 | },
84 | {
85 | id: '6',
86 | type: 'pr',
87 | status: 'failure',
88 | timestamp: '2023-11-28T17:00:00Z',
89 | url: 'https://github.com/example/6',
90 | title: 'Test PR 2',
91 | description: 'Failed PR',
92 | },
93 | ];
94 |
95 | it('renders chart component', () => {
96 | const { getByTestId } = render(
97 |
98 | );
99 |
100 | expect(getByTestId('vega-lite-chart')).toBeInTheDocument();
101 | });
102 |
103 | it('filters activities by type', () => {
104 | render();
105 | const lastCall = getLastVegaLiteProps();
106 | const chartData = lastCall.spec.data.values;
107 |
108 | // Should only include issue activities
109 | expect(chartData).toHaveLength(4);
110 | expect(chartData.every((d: { date: string; status: string }) =>
111 | ['no_pr', 'pr_open', 'pr_merged', 'pr_closed'].includes(d.status)
112 | )).toBe(true);
113 | });
114 |
115 | it('configures issue color scale correctly', () => {
116 | render();
117 | const lastCall = getLastVegaLiteProps();
118 | const colorScale = lastCall.spec.encoding.color.scale;
119 |
120 | expect(colorScale.domain).toEqual(['no_pr', 'pr_open', 'pr_merged', 'pr_closed']);
121 | expect(colorScale.range).toEqual(['#ffffff', '#4caf50', '#9c27b0', '#f44336']);
122 | });
123 |
124 | it('configures PR color scale correctly', () => {
125 | render();
126 | const lastCall = getLastVegaLiteProps();
127 | const colorScale = lastCall.spec.encoding.color.scale;
128 |
129 | expect(colorScale.domain).toEqual(['success', 'failure']);
130 | expect(colorScale.range).toEqual(['#22c55e', '#ef4444']);
131 | });
132 |
133 | it('configures chart axes correctly', () => {
134 | render();
135 | const lastCall = getLastVegaLiteProps();
136 | const { x, y } = lastCall.spec.encoding;
137 |
138 | expect(x.field).toBe('date');
139 | expect(x.type).toBe('temporal');
140 | expect(x.title).toBe('Date');
141 | expect(x.axis).toEqual({
142 | labelColor: '#C4CBDA',
143 | titleColor: '#C4CBDA',
144 | gridColor: '#3c3c4a',
145 | domainColor: '#3c3c4a'
146 | });
147 |
148 | expect(y.aggregate).toBe('count');
149 | expect(y.type).toBe('quantitative');
150 | expect(y.title).toBe('Count');
151 | expect(y.axis).toEqual({
152 | labelColor: '#C4CBDA',
153 | titleColor: '#C4CBDA',
154 | gridColor: '#3c3c4a',
155 | domainColor: '#3c3c4a'
156 | });
157 | });
158 |
159 | it('configures chart title correctly', () => {
160 | render();
161 | const lastCall = getLastVegaLiteProps();
162 | const { title } = lastCall.spec;
163 |
164 | expect(title).toEqual({
165 | text: 'ISSUE Activity Over Time',
166 | color: '#C4CBDA'
167 | });
168 | });
169 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OpenHands Bot Activity Monitor
2 |
3 | A web application to monitor and visualize the activity of the OpenHands GitHub bot.
4 |
5 | ## Features
6 |
7 | ### 1. Activity Listing
8 | - Lists all attempted issue resolutions and PR modifications
9 | - Entries are sorted by date
10 | - Each entry shows:
11 | - Type (Issue/PR)
12 | - Status (Success/Failure)
13 | - Timestamp
14 | - Link to the relevant GitHub item
15 | - Description of the action taken
16 |
17 | ### 2. Filtering Capabilities
18 | - Filter activities by:
19 | - Type (Issue resolution or PR modification)
20 | - Status (Success or Failure)
21 | - Date range
22 |
23 | ### 3. Visualization
24 | - Stacked line charts implemented using Vega-Lite showing:
25 | - Issue resolutions over time (successes vs failures)
26 | - PR modifications over time (successes vs failures)
27 | - Time-based trends in bot activity
28 |
29 | ## Prerequisites
30 |
31 | - Node.js 18 or later
32 | - npm 9 or later
33 | - A GitHub Personal Access Token with the following permissions:
34 | - `repo` scope for accessing repository data
35 | - `read:org` scope if monitoring repositories in an organization
36 |
37 | ## Environment Variables
38 |
39 | The application uses environment variables for configuration:
40 |
41 | ### Build Time
42 | - `GITHUB_TOKEN` - GitHub Personal Access Token (required for cache generation)
43 | - Required scopes: `repo` for repository access, `read:org` for organization repositories
44 | - Only needed during cache generation, not at runtime
45 |
46 | You can set this in a `.env` file for local development:
47 | ```env
48 | GITHUB_TOKEN=your_github_token_here
49 | ```
50 |
51 | Note: Never commit your `.env` file to version control. The `.gitignore` file already includes it.
52 |
53 | ## Installation
54 |
55 | 1. Clone the repository:
56 | ```bash
57 | git clone https://github.com/All-Hands-AI/openhands-agent-monitor.git
58 | cd openhands-agent-monitor
59 | ```
60 |
61 | 2. Install dependencies:
62 | ```bash
63 | npm install
64 | ```
65 |
66 | ## Development
67 |
68 | 1. Generate the data cache:
69 | ```bash
70 | # Make sure GITHUB_TOKEN is set in your .env file
71 | npm run build:cache
72 | ```
73 |
74 | 2. Start the development server:
75 | ```bash
76 | npm run dev
77 | ```
78 |
79 | 3. Open your browser and navigate to `http://localhost:5173`
80 |
81 | Note: The cache is static during development. Run `npm run build:cache` again to refresh the data.
82 |
83 | ## Testing
84 |
85 | Run the test suite:
86 | ```bash
87 | # Run tests in watch mode
88 | npm run test
89 |
90 | # Run tests with coverage report
91 | npm run test:coverage
92 | ```
93 |
94 | ## Building and Deployment
95 |
96 | The application uses a caching mechanism to improve performance and security. Instead of making GitHub API calls from the frontend, the data is pre-fetched and cached during the build process.
97 |
98 | ### Build Commands
99 |
100 | - `npm run build` - Build the frontend application only
101 | - `npm run build:cache` - Generate the GitHub data cache
102 | - `npm run build:all` - Generate cache and build the application
103 |
104 | ### Deployment Process
105 |
106 | 1. Set up environment:
107 | ```bash
108 | # Clone repository and install dependencies
109 | git clone https://github.com/All-Hands-AI/openhands-agent-monitor.git
110 | cd openhands-agent-monitor
111 | npm install
112 | ```
113 |
114 | 2. Generate cache and build:
115 | ```bash
116 | # Set GitHub token for cache generation
117 | export GITHUB_TOKEN=your_github_token_here
118 |
119 | # Generate cache and build application
120 | npm run build:all
121 | ```
122 |
123 | 3. Deploy the `dist` directory to your hosting provider.
124 |
125 | Note: The GitHub token is only needed during the build process to generate the cache. The frontend application reads from this cache and does not need the token at runtime.
126 |
127 | ### Static Hosting (e.g., GitHub Pages, Netlify)
128 |
129 | 1. Set up deployment:
130 | ```bash
131 | # Add build command in your hosting provider
132 | npm run build
133 |
134 | # Add cache generation to your build pipeline
135 | npm run build:cache
136 | ```
137 |
138 | 2. Configure environment variables:
139 | - Set `GITHUB_TOKEN` in your CI/CD environment
140 | - No environment variables needed in the hosting environment
141 |
142 | ### Docker Deployment
143 |
144 | 1. Build the Docker image:
145 | ```bash
146 | # Build with cache generation
147 | docker build --build-arg GITHUB_TOKEN=your_token_here -t openhands-monitor .
148 | ```
149 |
150 | 2. Run the container:
151 | ```bash
152 | # No token needed at runtime
153 | docker run -p 8080:80 openhands-monitor
154 | ```
155 |
156 | The app will be available at `http://localhost:8080`.
157 |
158 | ## Configuration
159 |
160 | The application is configured to monitor the OpenHands repository by default. To monitor a different repository, modify the following constants in `src/services/github.ts`:
161 |
162 | ```typescript
163 | const REPO_OWNER = 'your-org-name';
164 | const REPO_NAME = 'your-repo-name';
165 | ```
166 |
167 | ## Troubleshooting
168 |
169 | ### Common Issues
170 |
171 | 1. **Cache Generation Fails**
172 | - Symptom: Error during `npm run build:cache`
173 | - Solution:
174 | - Verify `GITHUB_TOKEN` is set and has correct permissions
175 | - Check for GitHub API rate limiting
176 | - Ensure repository configuration is correct
177 |
178 | 2. **No Data Showing**
179 | - Symptom: Empty activity list
180 | - Solution:
181 | - Verify cache was generated successfully
182 | - Check date range filter settings
183 | - Run `npm run build:cache` to refresh data
184 |
185 | 3. **Development Server Shows Old Data**
186 | - Symptom: Changes in GitHub not reflected
187 | - Solution: Run `npm run build:cache` to update the cache
188 |
189 | ### Getting Help
190 |
191 | If you encounter issues:
192 |
193 | 1. Check the browser console for error messages
194 | 2. Verify environment variables are set correctly
195 | 3. Ensure GitHub token has required permissions
196 | 4. Open an issue in the repository with:
197 | - Description of the problem
198 | - Steps to reproduce
199 | - Error messages (if any)
200 | - Environment details (OS, browser, Node.js version)
201 |
202 | ## Contributing
203 |
204 | 1. Fork the repository
205 | 2. Create a feature branch
206 | 3. Make your changes
207 | 4. Write or update tests
208 | 5. Submit a pull request
209 |
210 | ## License
211 |
212 | This project is licensed under the MIT License - see the LICENSE file for details.
213 | ```
214 |
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --bg-dark: #0c0e10;
3 | --bg-light: #292929;
4 | --bg-input: #393939;
5 | --bg-workspace: #1f2228;
6 | --border: #3c3c4a;
7 | --text-editor-base: #9099AC;
8 | --text-editor-active: #C4CBDA;
9 | --bg-editor-sidebar: #24272E;
10 | --bg-editor-active: #31343D;
11 | --border-editor-sidebar: #3C3C4A;
12 | --bg-neutral-muted: #afb8c133;
13 | }
14 |
15 | body {
16 | background-color: var(--bg-dark);
17 | color: var(--text-editor-active);
18 | }
19 |
20 | .app {
21 | max-width: 1200px;
22 | margin: 0 auto;
23 | padding: 1rem;
24 | }
25 |
26 | .app-header {
27 | display: flex;
28 | align-items: center;
29 | gap: 1rem;
30 | margin-bottom: 2rem;
31 | flex-wrap: wrap;
32 | justify-content: center;
33 | }
34 |
35 | .app-logo {
36 | height: 40px;
37 | width: auto;
38 | }
39 |
40 | h1 {
41 | text-align: center;
42 | margin-bottom: 1rem;
43 | color: var(--text-editor-active);
44 | font-size: clamp(1.5rem, 4vw, 2rem);
45 | }
46 |
47 | @media (max-width: 768px) {
48 | .app {
49 | padding: 0.5rem;
50 | }
51 |
52 | .app-header {
53 | flex-direction: column;
54 | text-align: center;
55 | }
56 | }
57 |
58 | section {
59 | margin-bottom: 3rem;
60 | }
61 |
62 | .filters {
63 | background: var(--bg-workspace);
64 | padding: 1.5rem;
65 | border-radius: 8px;
66 | border: 1px solid var(--border);
67 | }
68 |
69 | .activity-filter {
70 | display: flex;
71 | gap: 1rem;
72 | justify-content: center;
73 | margin-bottom: 1rem;
74 | flex-wrap: wrap;
75 | }
76 |
77 | .date-range-filter {
78 | display: flex;
79 | gap: 1rem;
80 | justify-content: center;
81 | flex-wrap: wrap;
82 | }
83 |
84 | .filter-group {
85 | display: flex;
86 | align-items: center;
87 | gap: 0.5rem;
88 | margin: 0.5rem;
89 | }
90 |
91 | .filter-group select,
92 | .filter-group input {
93 | padding: 0.5rem;
94 | border: 1px solid var(--border);
95 | border-radius: 4px;
96 | font-size: 1rem;
97 | background: var(--bg-input);
98 | color: var(--text-editor-active);
99 | min-width: 120px;
100 | }
101 |
102 | .filter-group input[type="date"]::-webkit-calendar-picker-indicator {
103 | filter: invert(1);
104 | cursor: pointer;
105 | }
106 |
107 | /* Calendar popup styling */
108 | input[type="date"]::-webkit-datetime-edit,
109 | input[type="date"]::-webkit-inner-spin-button,
110 | input[type="date"]::-webkit-clear-button {
111 | color: var(--text-editor-active);
112 | }
113 |
114 | input[type="date"]::-webkit-calendar-picker {
115 | background-color: var(--bg-workspace);
116 | color: var(--text-editor-active);
117 | border: 1px solid var(--border);
118 | border-radius: 4px;
119 | }
120 |
121 | @media (max-width: 768px) {
122 | .activity-filter,
123 | .date-range-filter {
124 | flex-direction: column;
125 | align-items: stretch;
126 | gap: 0.5rem;
127 | }
128 |
129 | .filter-group {
130 | margin: 0.25rem 0;
131 | justify-content: space-between;
132 | }
133 |
134 | .filter-group select,
135 | .filter-group input {
136 | flex: 1;
137 | margin-left: 0.5rem;
138 | }
139 | }
140 |
141 | .chart-container {
142 | display: flex;
143 | flex-wrap: wrap;
144 | gap: 2rem;
145 | justify-content: center;
146 | background: var(--bg-workspace);
147 | padding: 1rem;
148 | border-radius: 8px;
149 | border: 1px solid var(--border);
150 | overflow-x: auto;
151 | }
152 |
153 | .activity-list {
154 | display: flex;
155 | flex-direction: column;
156 | gap: 1rem;
157 | }
158 |
159 | .activity-item {
160 | border: 1px solid var(--border);
161 | border-radius: 8px;
162 | padding: 1rem;
163 | background: var(--bg-workspace);
164 | word-break: break-word;
165 | }
166 |
167 | .activity-item.success {
168 | border-left: 4px solid #4caf50;
169 | }
170 |
171 | .activity-item.failure {
172 | border-left: 4px solid var(--danger);
173 | }
174 |
175 | /* Issue-specific status colors */
176 | .activity-item.no_pr {
177 | border-left: 4px solid #ffffff;
178 | }
179 |
180 | .activity-item.pr_open {
181 | border-left: 4px solid #4caf50;
182 | }
183 |
184 | .activity-item.pr_merged {
185 | border-left: 4px solid #9c27b0;
186 | }
187 |
188 | .activity-item.pr_closed {
189 | border-left: 4px solid #f44336;
190 | }
191 |
192 | .activity-header {
193 | display: flex;
194 | gap: 1rem;
195 | margin-bottom: 0.5rem;
196 | flex-wrap: wrap;
197 | }
198 |
199 | @media (max-width: 768px) {
200 | .chart-container {
201 | padding: 0.5rem;
202 | gap: 1rem;
203 | }
204 |
205 | .activity-item {
206 | padding: 0.75rem;
207 | }
208 |
209 | .activity-header {
210 | gap: 0.5rem;
211 | }
212 | }
213 |
214 | .activity-type {
215 | font-weight: bold;
216 | color: var(--text-editor-active);
217 | }
218 |
219 | .activity-status {
220 | text-transform: capitalize;
221 | }
222 |
223 | .activity-time {
224 | color: var(--text-editor-base);
225 | }
226 |
227 | .activity-description {
228 | margin: 0.5rem 0;
229 | color: var(--text-editor-base);
230 | }
231 |
232 | .activity-item a {
233 | color: var(--hyperlink);
234 | text-decoration: none;
235 | }
236 |
237 | .activity-item a:hover {
238 | text-decoration: underline;
239 | }
240 |
241 | .loading-spinner {
242 | display: flex;
243 | flex-direction: column;
244 | align-items: center;
245 | justify-content: center;
246 | padding: 2rem;
247 | }
248 |
249 | .spinner {
250 | width: 50px;
251 | height: 50px;
252 | border: 5px solid var(--bg-input);
253 | border-top: 5px solid var(--hyperlink);
254 | border-radius: 50%;
255 | animation: spin 1s linear infinite;
256 | margin-bottom: 1rem;
257 | }
258 |
259 | @keyframes spin {
260 | 0% { transform: rotate(0deg); }
261 | 100% { transform: rotate(360deg); }
262 | }
263 |
264 | .error-message {
265 | text-align: center;
266 | padding: 2rem;
267 | background: var(--bg-workspace);
268 | border: 1px solid var(--danger);
269 | border-radius: 8px;
270 | margin: 1rem 0;
271 | }
272 |
273 | .error-message p {
274 | color: var(--danger);
275 | margin-bottom: 1rem;
276 | }
277 |
278 | .error-message button {
279 | background: var(--hyperlink);
280 | color: white;
281 | border: none;
282 | padding: 0.5rem 1rem;
283 | border-radius: 4px;
284 | cursor: pointer;
285 | font-size: 1rem;
286 | }
287 |
288 | .error-message button:hover {
289 | opacity: 0.9;
290 | }
291 |
292 | /* Pagination styles */
293 | .pagination {
294 | display: flex;
295 | justify-content: center;
296 | align-items: center;
297 | gap: 1rem;
298 | margin-top: 2rem;
299 | }
300 |
301 | .pagination button {
302 | padding: 0.5rem 1rem;
303 | background: var(--bg-input);
304 | color: var(--text-editor-active);
305 | border: 1px solid var(--border);
306 | border-radius: 4px;
307 | cursor: pointer;
308 | transition: background-color 0.2s;
309 | }
310 |
311 | .pagination button:disabled {
312 | opacity: 0.5;
313 | cursor: not-allowed;
314 | }
315 |
316 | .pagination button:not(:disabled):hover {
317 | background: var(--bg-editor-active);
318 | }
319 |
320 | .pagination .page-info {
321 | color: var(--text-editor-base);
322 | padding: 0.5rem 1rem;
323 | }
324 |
--------------------------------------------------------------------------------
/src/components/DateRangeFilter.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent, act } from '@testing-library/react';
2 | import { describe, it, expect, vi, beforeEach } from 'vitest';
3 | import { DateRangeFilter } from './DateRangeFilter';
4 | import { DateRange } from '../types';
5 | import { getComputedStyle } from '../test/testUtils';
6 |
7 | describe('DateRangeFilter', () => {
8 | const mockDateRange: DateRange = {
9 | start: '2023-11-01',
10 | end: '2023-11-30',
11 | };
12 | const mockOnDateRangeChange = vi.fn();
13 |
14 | beforeEach(() => {
15 | mockOnDateRangeChange.mockClear();
16 | });
17 |
18 | it('renders date inputs with provided values', () => {
19 | render(
20 |
24 | );
25 |
26 | const startInput = screen.getByLabelText('From:');
27 | const endInput = screen.getByLabelText('To:');
28 |
29 | expect(startInput.value).toBe('2023-11-01');
30 | expect(endInput.value).toBe('2023-11-30');
31 | });
32 |
33 | it('handles start date changes', () => {
34 | render(
35 |
39 | );
40 |
41 | const startInput = screen.getByLabelText('From:');
42 | fireEvent.change(startInput, { target: { value: '2023-11-15' } });
43 |
44 | expect(mockOnDateRangeChange).toHaveBeenCalledWith({
45 | start: '2023-11-15',
46 | end: '2023-11-30',
47 | });
48 | });
49 |
50 | it('handles end date changes', () => {
51 | render(
52 |
56 | );
57 |
58 | const endInput = screen.getByLabelText('To:');
59 | fireEvent.change(endInput, { target: { value: '2023-11-20' } });
60 |
61 | expect(mockOnDateRangeChange).toHaveBeenCalledWith({
62 | start: '2023-11-01',
63 | end: '2023-11-20',
64 | });
65 | });
66 |
67 | it('handles clearing dates', () => {
68 | render(
69 |
73 | );
74 |
75 | const startInput = screen.getByLabelText('From:');
76 | const endInput = screen.getByLabelText('To:');
77 |
78 | // Clear both inputs
79 | fireEvent.change(startInput, { target: { value: '' } });
80 | fireEvent.change(endInput, { target: { value: '' } });
81 | expect(mockOnDateRangeChange).toHaveBeenLastCalledWith(undefined);
82 | });
83 |
84 | it('initializes with default 7-day range when no dateRange is provided', () => {
85 | vi.useFakeTimers();
86 | const now = new Date('2024-01-15T12:00:00Z');
87 | vi.setSystemTime(now);
88 |
89 | const mockOnDateRangeChange = vi.fn();
90 |
91 | act(() => {
92 | render(
93 |
97 | );
98 | });
99 |
100 | const startInput = screen.getByLabelText('From:');
101 | const endInput = screen.getByLabelText('To:');
102 |
103 | // Check that inputs show 7-day range
104 | expect(startInput.value).toBe('2024-01-08');
105 | expect(endInput.value).toBe('2024-01-15');
106 |
107 | // Check that onDateRangeChange was called with default range
108 | expect(mockOnDateRangeChange).toHaveBeenCalledWith({
109 | start: '2024-01-08',
110 | end: '2024-01-15'
111 | });
112 |
113 | vi.useRealTimers();
114 | });
115 |
116 | it('maintains provided dateRange over default range', () => {
117 | const customDateRange = {
118 | start: '2024-01-01',
119 | end: '2024-01-31'
120 | };
121 |
122 | render(
123 |
127 | );
128 |
129 | const startInput = screen.getByLabelText('From:');
130 | const endInput = screen.getByLabelText('To:');
131 |
132 | expect(startInput.value).toBe('2024-01-01');
133 | expect(endInput.value).toBe('2024-01-31');
134 | expect(mockOnDateRangeChange).not.toHaveBeenCalled();
135 | });
136 |
137 | describe('dark theme styling', () => {
138 | beforeEach(() => {
139 | // Set up CSS variables for dark theme
140 | document.documentElement.style.setProperty('--bg-input', '#393939');
141 | document.documentElement.style.setProperty('--text-editor-active', '#C4CBDA');
142 | document.documentElement.style.setProperty('--border', '#3c3c4a');
143 | });
144 |
145 | it('applies dark theme styles to date inputs', () => {
146 | render(
147 |
151 | );
152 |
153 | // Check if the style rules exist with correct variables
154 | const styleRules = Array.from(document.styleSheets)
155 | .flatMap(sheet => Array.from(sheet.cssRules))
156 | .map(rule => rule.cssText)
157 | .join('\n');
158 |
159 | expect(styleRules).toContain('.filter-group input');
160 | expect(styleRules).toContain('var(--bg-input)');
161 | expect(styleRules).toContain('var(--text-editor-active)');
162 | expect(styleRules).toContain('var(--border)');
163 | });
164 |
165 | it('has proper spacing between filter groups', () => {
166 | render(
167 |
171 | );
172 |
173 | const filterGroups = document.querySelectorAll('.filter-group');
174 | const firstGroup = filterGroups[0] as HTMLElement;
175 |
176 | // Check gap between filter groups
177 | expect(getComputedStyle(firstGroup, 'gap')).toBe('0.5rem');
178 | expect(getComputedStyle(firstGroup, 'margin')).toBe('0.5rem');
179 | });
180 |
181 | it('has proper input padding and border radius', () => {
182 | render(
183 |
187 | );
188 |
189 | const startInput = screen.getByLabelText('From:');
190 |
191 | expect(getComputedStyle(startInput, 'padding')).toBe('0.5rem');
192 | expect(getComputedStyle(startInput, 'border-radius')).toBe('4px');
193 | });
194 |
195 | it('calendar picker indicator is visible in dark mode', () => {
196 | render(
197 |
201 | );
202 |
203 | screen.getByLabelText('From:');
204 |
205 | // Check if the calendar picker indicator style exists
206 | const styleRules = Array.from(document.styleSheets)
207 | .flatMap(sheet => Array.from(sheet.cssRules))
208 | .map(rule => rule.cssText)
209 | .join('\n');
210 |
211 | expect(styleRules).toContain('::-webkit-calendar-picker-indicator');
212 | expect(styleRules).toContain('filter: invert(1)');
213 | });
214 | });
215 | });
--------------------------------------------------------------------------------
/src/services/github.test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Unit tests for the GitHub service.
3 | *
4 | * These tests use mocked API responses to verify the service's behavior
5 | * without making actual network requests. They focus on testing:
6 | *
7 | * 1. Comment detection logic for both issues and PRs
8 | * 2. Success/failure status determination
9 | * 3. Activity data structure and formatting
10 | *
11 | * The tests use Vitest's mocking capabilities to simulate GitHub API responses.
12 | */
13 |
14 | import { describe, it, expect, beforeEach, vi } from 'vitest';
15 | import { fetchBotActivities } from './github';
16 | import type { BotActivity } from '../types';
17 |
18 | describe('GitHub Service', () => {
19 | beforeEach(() => {
20 | // Mock the fetch function
21 | vi.stubGlobal('fetch', vi.fn());
22 | });
23 |
24 | function createMockResponse(data: unknown): Promise {
25 | return Promise.resolve({
26 | ok: true,
27 | status: 200,
28 | statusText: 'OK',
29 | json: () => Promise.resolve(data),
30 | headers: new Headers()
31 | } as Response);
32 | }
33 |
34 | it('should detect openhands-agent comments in issues with PR', async () => {
35 | // Mock cache response for issue success
36 | const mockFetch = vi.fn().mockImplementation((url: string) => {
37 | if (url === '/cache/bot-activities.json') {
38 | return createMockResponse({
39 | activities: [{
40 | id: 'issue-1-2',
41 | type: 'issue',
42 | status: 'pr_open',
43 | timestamp: '2023-11-28T00:01:00Z',
44 | url: 'https://github.com/All-Hands-AI/OpenHands/issues/1#comment-2',
45 | prUrl: 'https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/2',
46 | description: 'A potential fix has been generated and a draft PR #2 has been created. Please review the changes.'
47 | }],
48 | lastUpdated: '2023-11-28T00:01:00Z'
49 | });
50 | }
51 | throw new Error(`Unexpected URL: ${url}`);
52 | });
53 | vi.stubGlobal('fetch', mockFetch as unknown as typeof fetch);
54 |
55 | const activities = await fetchBotActivities();
56 |
57 | expect(activities).toHaveLength(1);
58 | expect(activities[0]).toMatchObject>({
59 | type: 'issue',
60 | status: 'pr_open',
61 | id: expect.stringContaining('issue-1') as string,
62 | });
63 |
64 | // Restore the original fetch
65 | vi.unstubAllGlobals();
66 | });
67 |
68 | it('should detect openhands-agent failure comments in issues without PR', async () => {
69 | // Mock cache response for issue failure
70 | const mockFetch = vi.fn().mockImplementation((url: string) => {
71 | if (url === '/cache/bot-activities.json') {
72 | return createMockResponse({
73 | activities: [{
74 | id: 'issue-1-2',
75 | type: 'issue',
76 | status: 'no_pr',
77 | timestamp: '2023-11-28T00:01:00Z',
78 | url: 'https://github.com/All-Hands-AI/OpenHands/issues/1#comment-2',
79 | description: 'The workflow to fix this issue encountered an error. Openhands failed to create any code changes.'
80 | }],
81 | lastUpdated: '2023-11-28T00:01:00Z'
82 | });
83 | }
84 | throw new Error(`Unexpected URL: ${url}`);
85 | });
86 | vi.stubGlobal('fetch', mockFetch as unknown as typeof fetch);
87 |
88 | const activities = await fetchBotActivities();
89 |
90 | expect(activities).toHaveLength(1);
91 | expect(activities[0]).toMatchObject>({
92 | type: 'issue',
93 | status: 'no_pr',
94 | id: expect.stringContaining('issue-1') as string,
95 | });
96 |
97 | // Restore the original fetch
98 | vi.unstubAllGlobals();
99 | });
100 |
101 | it('should detect openhands-agent failure comments in PRs', async () => {
102 | // Mock cache response for PR failure
103 | const mockFetch = vi.fn().mockImplementation((url: string) => {
104 | if (url === '/cache/bot-activities.json') {
105 | return createMockResponse({
106 | activities: [{
107 | id: 'pr-1-2',
108 | type: 'pr',
109 | status: 'failure',
110 | timestamp: '2023-11-28T00:01:00Z',
111 | url: 'https://github.com/All-Hands-AI/OpenHands/pull/1#comment-2',
112 | description: 'The workflow to fix this issue encountered an error. Openhands failed to create any code changes.'
113 | }],
114 | lastUpdated: '2023-11-28T00:01:00Z'
115 | });
116 | }
117 | throw new Error(`Unexpected URL: ${url}`);
118 | });
119 | vi.stubGlobal('fetch', mockFetch as unknown as typeof fetch);
120 |
121 | const activities = await fetchBotActivities();
122 |
123 | expect(activities).toHaveLength(1);
124 | expect(activities[0]).toMatchObject>({
125 | type: 'pr',
126 | status: 'failure',
127 | id: expect.stringContaining('pr-1') as string,
128 | });
129 |
130 | // Restore the original fetch
131 | vi.unstubAllGlobals();
132 | });
133 |
134 | it('should handle issue with no PR', async () => {
135 | const mockFetch = vi.fn().mockImplementation((url: string) => {
136 | if (url === '/cache/bot-activities.json') {
137 | return createMockResponse({
138 | activities: [{
139 | id: 'issue-1-2',
140 | type: 'issue',
141 | status: 'no_pr',
142 | timestamp: '2023-11-28T00:01:00Z',
143 | url: 'https://github.com/All-Hands-AI/OpenHands/issues/1#comment-2',
144 | description: 'Working on the issue...'
145 | }],
146 | lastUpdated: '2023-11-28T00:01:00Z'
147 | });
148 | }
149 | throw new Error(`Unexpected URL: ${url}`);
150 | });
151 | vi.stubGlobal('fetch', mockFetch as unknown as typeof fetch);
152 |
153 | const activities = await fetchBotActivities();
154 |
155 | expect(activities).toHaveLength(1);
156 | expect(activities[0]).toMatchObject>({
157 | type: 'issue',
158 | status: 'no_pr',
159 | });
160 |
161 | vi.unstubAllGlobals();
162 | });
163 |
164 | it('should handle issue with open PR', async () => {
165 | const mockFetch = vi.fn().mockImplementation((url: string) => {
166 | if (url === '/cache/bot-activities.json') {
167 | return createMockResponse({
168 | activities: [{
169 | id: 'issue-1-2',
170 | type: 'issue',
171 | status: 'pr_open',
172 | timestamp: '2023-11-28T00:01:00Z',
173 | url: 'https://github.com/All-Hands-AI/OpenHands/issues/1#comment-2',
174 | prUrl: 'https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/2',
175 | description: 'Created PR #2'
176 | }],
177 | lastUpdated: '2023-11-28T00:01:00Z'
178 | });
179 | }
180 | throw new Error(`Unexpected URL: ${url}`);
181 | });
182 | vi.stubGlobal('fetch', mockFetch as unknown as typeof fetch);
183 |
184 | const activities = await fetchBotActivities();
185 |
186 | expect(activities).toHaveLength(1);
187 | expect(activities[0]).toMatchObject>({
188 | type: 'issue',
189 | status: 'pr_open',
190 | });
191 |
192 | vi.unstubAllGlobals();
193 | });
194 |
195 | it('should handle issue with merged PR', async () => {
196 | const mockFetch = vi.fn().mockImplementation((url: string) => {
197 | if (url === '/cache/bot-activities.json') {
198 | return createMockResponse({
199 | activities: [{
200 | id: 'issue-1-2',
201 | type: 'issue',
202 | status: 'pr_merged',
203 | timestamp: '2023-11-28T00:01:00Z',
204 | url: 'https://github.com/All-Hands-AI/OpenHands/issues/1#comment-2',
205 | prUrl: 'https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/2',
206 | description: 'Created PR #2'
207 | }],
208 | lastUpdated: '2023-11-28T00:01:00Z'
209 | });
210 | }
211 | throw new Error(`Unexpected URL: ${url}`);
212 | });
213 | vi.stubGlobal('fetch', mockFetch as unknown as typeof fetch);
214 |
215 | const activities = await fetchBotActivities();
216 |
217 | expect(activities).toHaveLength(1);
218 | expect(activities[0]).toMatchObject>({
219 | type: 'issue',
220 | status: 'pr_merged',
221 | });
222 |
223 | vi.unstubAllGlobals();
224 | });
225 |
226 | it('should handle issue with closed PR', async () => {
227 | const mockFetch = vi.fn().mockImplementation((url: string) => {
228 | if (url === '/cache/bot-activities.json') {
229 | return createMockResponse({
230 | activities: [{
231 | id: 'issue-1-2',
232 | type: 'issue',
233 | status: 'pr_closed',
234 | timestamp: '2023-11-28T00:01:00Z',
235 | url: 'https://github.com/All-Hands-AI/OpenHands/issues/1#comment-2',
236 | prUrl: 'https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/2',
237 | description: 'Created PR #2'
238 | }],
239 | lastUpdated: '2023-11-28T00:01:00Z'
240 | });
241 | }
242 | throw new Error(`Unexpected URL: ${url}`);
243 | });
244 | vi.stubGlobal('fetch', mockFetch as unknown as typeof fetch);
245 |
246 | const activities = await fetchBotActivities();
247 |
248 | expect(activities).toHaveLength(1);
249 | expect(activities[0]).toMatchObject>({
250 | type: 'issue',
251 | status: 'pr_closed',
252 | });
253 |
254 | vi.unstubAllGlobals();
255 | });
256 |
257 | it('should detect openhands-agent comments in PRs', async () => {
258 | // Mock cache response for PR success
259 | const mockFetch = vi.fn().mockImplementation((url: string) => {
260 | if (url === '/cache/bot-activities.json') {
261 | return createMockResponse({
262 | activities: [{
263 | id: 'pr-1-2',
264 | type: 'pr',
265 | status: 'success',
266 | timestamp: '2023-11-28T00:01:00Z',
267 | url: 'https://github.com/All-Hands-AI/OpenHands/pull/1#comment-2',
268 | description: 'OpenHands made the following changes to resolve the issues:\n\n- Fixed the bug in the code\n\nUpdated pull request https://github.com/All-Hands-AI/OpenHands/pull/1 with new patches.'
269 | }],
270 | lastUpdated: '2023-11-28T00:01:00Z'
271 | });
272 | }
273 | throw new Error(`Unexpected URL: ${url}`);
274 | });
275 | vi.stubGlobal('fetch', mockFetch as unknown as typeof fetch);
276 |
277 | const activities = await fetchBotActivities();
278 |
279 | expect(activities).toHaveLength(1);
280 | expect(activities[0]).toMatchObject>({
281 | type: 'pr',
282 | status: 'success',
283 | id: expect.stringContaining('pr-1') as string,
284 | });
285 |
286 | // Restore the original fetch
287 | vi.unstubAllGlobals();
288 | });
289 | });
--------------------------------------------------------------------------------
/src/components/ActivityList.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent } from '@testing-library/react';
2 | import { describe, it, expect, beforeEach, afterEach } from 'vitest';
3 | import { ActivityList } from './ActivityList';
4 | import { BotActivity } from '../types';
5 | import { getComputedStyle } from '../test/testUtils';
6 |
7 | describe('ActivityList', () => {
8 | const createMockActivity = (id: string): BotActivity => ({
9 | id,
10 | type: 'issue',
11 | status: 'success',
12 | timestamp: '2023-11-28T12:00:00Z',
13 | url: `https://github.com/example/${id}`,
14 | title: `Test Issue ${id}`,
15 | description: `Description for issue ${id}`,
16 | });
17 |
18 | const mockActivities: BotActivity[] = [
19 | {
20 | id: '1',
21 | type: 'issue',
22 | status: 'success',
23 | timestamp: '2023-11-28T12:00:00Z',
24 | url: 'https://github.com/example/1',
25 | title: 'ISSUE success 11/28/2023, 12:00:00 PM -- Test Issue 1',
26 | description: 'Successfully resolved issue',
27 | },
28 | {
29 | id: '2',
30 | type: 'pr',
31 | status: 'failure',
32 | timestamp: '2023-11-28T13:00:00Z',
33 | url: 'https://github.com/example/2',
34 | title: 'PR failure 11/28/2023, 1:00:00 PM -- Test PR 1',
35 | description: 'Failed to modify PR',
36 | },
37 | ];
38 |
39 | it('renders activities correctly', () => {
40 | render();
41 |
42 | // Check if activities are rendered
43 | expect(screen.getByText('ISSUE success 11/28/2023, 12:00:00 PM -- Test Issue 1')).toBeInTheDocument();
44 | expect(screen.getByText('PR failure 11/28/2023, 1:00:00 PM -- Test PR 1')).toBeInTheDocument();
45 | expect(screen.getByText('Successfully resolved issue')).toBeInTheDocument();
46 | expect(screen.getByText('Failed to modify PR')).toBeInTheDocument();
47 |
48 | // Check if links are rendered correctly
49 | const links = screen.getAllByText('View on GitHub');
50 | expect(links).toHaveLength(2);
51 | expect(links[0]).toHaveAttribute('href', 'https://github.com/example/1');
52 | expect(links[1]).toHaveAttribute('href', 'https://github.com/example/2');
53 | });
54 |
55 | it('renders empty state correctly', () => {
56 | render();
57 | expect(screen.queryByText('View on GitHub')).not.toBeInTheDocument();
58 | });
59 |
60 | it('does not show pagination controls when there are 20 or fewer items', () => {
61 | const activities = Array.from({ length: 20 }, (_, i) => createMockActivity(String(i + 1)));
62 | render();
63 |
64 | expect(screen.queryByText('Previous')).not.toBeInTheDocument();
65 | expect(screen.queryByText('Next')).not.toBeInTheDocument();
66 | expect(screen.queryByText(/Page \d+ of \d+/)).not.toBeInTheDocument();
67 | });
68 |
69 | it('shows pagination controls when there are more than 20 items', () => {
70 | const activities = Array.from({ length: 25 }, (_, i) => createMockActivity(String(i + 1)));
71 | render();
72 |
73 | expect(screen.getByText('Previous')).toBeInTheDocument();
74 | expect(screen.getByText('Next')).toBeInTheDocument();
75 | expect(screen.getByText('Page 1 of 2')).toBeInTheDocument();
76 | });
77 |
78 | it('shows only 20 items per page', () => {
79 | const activities = Array.from({ length: 25 }, (_, i) => createMockActivity(String(i + 1)));
80 | render();
81 |
82 | const items = screen.getAllByText(/Test Issue \d+/);
83 | expect(items).toHaveLength(20);
84 | expect(screen.getByText('Test Issue 1')).toBeInTheDocument();
85 | expect(screen.getByText('Test Issue 20')).toBeInTheDocument();
86 | expect(screen.queryByText('Test Issue 21')).not.toBeInTheDocument();
87 | });
88 |
89 | it('navigates between pages correctly', () => {
90 | const activities = Array.from({ length: 25 }, (_, i) => createMockActivity(String(i + 1)));
91 | render();
92 |
93 | // Initial page
94 | expect(screen.getByText('Test Issue 1')).toBeInTheDocument();
95 | expect(screen.queryByText('Test Issue 21')).not.toBeInTheDocument();
96 |
97 | // Navigate to next page
98 | fireEvent.click(screen.getByText('Next'));
99 | expect(screen.queryByText('Test Issue 1')).not.toBeInTheDocument();
100 | expect(screen.getByText('Test Issue 21')).toBeInTheDocument();
101 | expect(screen.getByText('Page 2 of 2')).toBeInTheDocument();
102 |
103 | // Navigate back to first page
104 | fireEvent.click(screen.getByText('Previous'));
105 | expect(screen.getByText('Test Issue 1')).toBeInTheDocument();
106 | expect(screen.queryByText('Test Issue 21')).not.toBeInTheDocument();
107 | expect(screen.getByText('Page 1 of 2')).toBeInTheDocument();
108 | });
109 |
110 | it('disables pagination buttons appropriately', () => {
111 | const activities = Array.from({ length: 25 }, (_, i) => createMockActivity(String(i + 1)));
112 | render();
113 |
114 | // On first page, Previous should be disabled
115 | expect(screen.getByText('Previous')).toBeDisabled();
116 | expect(screen.getByText('Next')).not.toBeDisabled();
117 |
118 | // Navigate to last page
119 | fireEvent.click(screen.getByText('Next'));
120 | expect(screen.getByText('Previous')).not.toBeDisabled();
121 | expect(screen.getByText('Next')).toBeDisabled();
122 | });
123 |
124 | describe('issue status styling', () => {
125 | beforeEach(() => {
126 | // Add CSS styles for testing
127 | const style = document.createElement('style');
128 | style.textContent = `
129 | .activity-item.no_pr { border-left: 4px solid #ffffff; }
130 | .activity-item.pr_open { border-left: 4px solid #4caf50; }
131 | .activity-item.pr_merged { border-left: 4px solid #9c27b0; }
132 | .activity-item.pr_closed { border-left: 4px solid #f44336; }
133 | `;
134 | document.head.appendChild(style);
135 | });
136 |
137 | afterEach(() => {
138 | // Clean up styles
139 | const styles = document.head.getElementsByTagName('style');
140 | Array.from(styles).forEach((style) => { style.remove(); });
141 | });
142 |
143 | const issueStatuses = [
144 | { status: 'no_pr', color: '#ffffff' },
145 | { status: 'pr_open', color: '#4caf50' },
146 | { status: 'pr_merged', color: '#9c27b0' },
147 | { status: 'pr_closed', color: '#f44336' }
148 | ];
149 |
150 | issueStatuses.forEach(({ status, color }) => {
151 | it(`applies correct border color for issue with ${status} status`, () => {
152 | const activity: BotActivity = {
153 | id: '1',
154 | type: 'issue',
155 | status: status as any,
156 | timestamp: '2023-11-28T12:00:00Z',
157 | url: 'https://github.com/example/1',
158 | title: `Test Issue with ${status}`,
159 | description: 'Test description',
160 | prUrl: status === 'no_pr' ? undefined : 'https://github.com/example/pr/1'
161 | };
162 |
163 | render();
164 |
165 | const item = document.querySelector('.activity-item') as HTMLElement;
166 | expect(item).not.toBeNull();
167 | expect(item.classList.contains(status)).toBe(true);
168 |
169 | const styleRules = Array.from(document.styleSheets)
170 | .flatMap(sheet => Array.from(sheet.cssRules))
171 | .map(rule => rule.cssText)
172 | .join('\n');
173 |
174 | expect(styleRules).toContain(`.activity-item.${status}`);
175 | expect(styleRules).toContain(`border-left: 4px solid ${color}`);
176 | });
177 |
178 | if (status !== 'no_pr') {
179 | it(`shows PR link for issue with ${status} status`, () => {
180 | const activity: BotActivity = {
181 | id: '1',
182 | type: 'issue',
183 | status: status as any,
184 | timestamp: '2023-11-28T12:00:00Z',
185 | url: 'https://github.com/example/1',
186 | title: `Test Issue with ${status}`,
187 | description: 'Test description',
188 | prUrl: 'https://github.com/example/pr/1'
189 | };
190 |
191 | render();
192 |
193 | const prLink = screen.getByText('View PR');
194 | expect(prLink).toBeInTheDocument();
195 | expect(prLink).toHaveAttribute('href', 'https://github.com/example/pr/1');
196 | });
197 | }
198 | });
199 | });
200 |
201 | describe('dark theme styling', () => {
202 | beforeEach(() => {
203 | // Set up CSS variables for dark theme
204 | document.documentElement.style.setProperty('--bg-input', '#393939');
205 | document.documentElement.style.setProperty('--text-editor-active', '#C4CBDA');
206 | document.documentElement.style.setProperty('--border', '#3c3c4a');
207 | document.documentElement.style.setProperty('--bg-editor-active', '#31343D');
208 | document.documentElement.style.setProperty('--text-editor-base', '#9099AC');
209 |
210 | // Add CSS styles for testing
211 | const style = document.createElement('style');
212 | style.textContent = `
213 | .pagination {
214 | display: flex;
215 | justify-content: center;
216 | align-items: center;
217 | gap: 1rem;
218 | margin-top: 2rem;
219 | }
220 |
221 | .pagination button {
222 | padding: 0.5rem 1rem;
223 | background: var(--bg-input);
224 | color: var(--text-editor-active);
225 | border: 1px solid var(--border);
226 | border-radius: 4px;
227 | cursor: pointer;
228 | transition: background-color 0.2s;
229 | }
230 |
231 | .pagination button:disabled {
232 | opacity: 0.5;
233 | cursor: not-allowed;
234 | }
235 |
236 | .pagination button:not(:disabled):hover {
237 | background: var(--bg-editor-active);
238 | }
239 |
240 | .pagination .page-info {
241 | color: var(--text-editor-base);
242 | padding: 0.5rem 1rem;
243 | }
244 | `;
245 | document.head.appendChild(style);
246 | });
247 |
248 | afterEach(() => {
249 | // Clean up styles
250 | const styles = document.head.getElementsByTagName('style');
251 | Array.from(styles).forEach((style) => { style.remove(); });
252 | });
253 |
254 | describe('pagination styling', () => {
255 | const activities = Array.from({ length: 25 }, (_, i) => createMockActivity(String(i + 1)));
256 |
257 | it('applies dark theme styles to pagination buttons', () => {
258 | render();
259 |
260 | // Check if the style rules exist with correct variables
261 | const styleRules = Array.from(document.styleSheets)
262 | .flatMap(sheet => Array.from(sheet.cssRules))
263 | .map(rule => rule.cssText)
264 | .join('\n');
265 |
266 | expect(styleRules).toContain('.pagination button');
267 | expect(styleRules).toContain('var(--bg-input)');
268 | expect(styleRules).toContain('var(--text-editor-active)');
269 | expect(styleRules).toContain('var(--border)');
270 | });
271 |
272 | it('has proper spacing between pagination elements', () => {
273 | render();
274 |
275 | const pagination = document.querySelector('.pagination') as HTMLElement;
276 | expect(pagination).not.toBeNull();
277 | expect(getComputedStyle(pagination, 'gap')).toBe('1rem');
278 | expect(getComputedStyle(pagination, 'margin-top')).toBe('2rem');
279 | });
280 |
281 | it('applies proper styles to disabled pagination buttons', () => {
282 | render();
283 |
284 | const prevButton = screen.getByText('Previous');
285 | expect(prevButton).toBeDisabled();
286 | expect(getComputedStyle(prevButton, 'opacity')).toBe('0.5');
287 | expect(getComputedStyle(prevButton, 'cursor')).toBe('not-allowed');
288 | });
289 |
290 | it('applies hover styles to enabled pagination buttons', () => {
291 | render();
292 |
293 | const nextButton = screen.getByText('Next');
294 | expect(nextButton).not.toBeDisabled();
295 |
296 | // Check if the hover style rule exists
297 | const styleRules = Array.from(document.styleSheets)
298 | .flatMap(sheet => Array.from(sheet.cssRules))
299 | .map(rule => rule.cssText)
300 | .join('\n');
301 |
302 | expect(styleRules).toContain('.pagination button:not(:disabled):hover');
303 | expect(styleRules).toContain('var(--bg-editor-active)');
304 | });
305 |
306 | it('styles page info text correctly', () => {
307 | render();
308 |
309 | const pageInfo = screen.getByText('Page 1 of 2');
310 | expect(pageInfo.classList.contains('page-info')).toBe(true);
311 |
312 | // Check if the style rule exists
313 | const styleRules = Array.from(document.styleSheets)
314 | .flatMap(sheet => Array.from(sheet.cssRules))
315 | .map(rule => rule.cssText)
316 | .join('\n');
317 |
318 | expect(styleRules).toContain('.pagination .page-info');
319 | expect(styleRules).toContain('var(--text-editor-base)');
320 | expect(styleRules).toContain('padding: 0.5rem 1rem');
321 | });
322 |
323 | it('has proper button padding and border radius', () => {
324 | render();
325 |
326 | const nextButton = screen.getByText('Next');
327 | expect(getComputedStyle(nextButton, 'padding')).toBe('0.5rem 1rem');
328 | expect(getComputedStyle(nextButton, 'border-radius')).toBe('4px');
329 | });
330 | });
331 | });
332 | });
--------------------------------------------------------------------------------
/scripts/src/github-api.ts:
--------------------------------------------------------------------------------
1 | import type { GitHubComment, GitHubIssue, GitHubPR, GitHubPRResponse, ApiResponse, Activity, IssueStatus, PRStatus } from './types';
2 | import fetch from 'node-fetch';
3 | import { performance } from 'perf_hooks';
4 |
5 | const GITHUB_TOKEN = process.env['GITHUB_TOKEN'] ?? '';
6 | const REPO_OWNER = 'All-Hands-AI';
7 | const REPO_NAME = 'OpenHands';
8 |
9 | import fs from 'fs';
10 |
11 | const MAX_RETRIES = 3;
12 | const RETRY_DELAY = 1000; // 1 second
13 |
14 | async function sleep(ms: number) {
15 | return new Promise(resolve => setTimeout(resolve, ms));
16 | }
17 |
18 | async function fetchWithAuth(url: string, retries = MAX_RETRIES): Promise> {
19 | // Log the request
20 | fs.appendFileSync('github-api.log', `\n[${new Date().toISOString()}] REQUEST: ${url}\n`);
21 |
22 | try {
23 | const response = await fetch(url, {
24 | headers: {
25 | 'Authorization': `Bearer ${GITHUB_TOKEN}`,
26 | 'Accept': 'application/vnd.github.v3+json',
27 | 'User-Agent': 'OpenHands-Agent-Monitor'
28 | },
29 | });
30 |
31 | // Check for rate limiting
32 | if (response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0') {
33 | const resetTime = parseInt(response.headers.get('x-ratelimit-reset') || '0') * 1000;
34 | const waitTime = Math.max(0, resetTime - Date.now());
35 | console.log(`Rate limited. Waiting ${String(waitTime)}ms before retrying...`);
36 | fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] RATE LIMIT: Waiting ${String(waitTime)}ms before retry\n`);
37 | await sleep(waitTime);
38 | return await fetchWithAuth(url, retries);
39 | }
40 |
41 | if (!response.ok) {
42 | const errorBody = await response.text();
43 | fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] ERROR: ${String(response.status)} ${String(response.statusText)}\n${String(errorBody)}\n`);
44 |
45 | if (retries > 0) {
46 | console.log(`Request failed. Retrying in ${String(RETRY_DELAY)}ms... (${String(retries)} retries left)`);
47 | fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] RETRY: ${String(retries)} attempts remaining\n`);
48 | await sleep(RETRY_DELAY);
49 | return await fetchWithAuth(url, retries - 1);
50 | }
51 |
52 | throw new Error(`GitHub API error: ${String(response.status)} ${String(response.statusText)}\n${String(errorBody)}`);
53 | }
54 |
55 | // Parse Link header for pagination
56 | const linkHeader = response.headers.get('Link') ?? '';
57 | let hasNextPage = false;
58 | let nextUrl: string | null = null;
59 |
60 | if (linkHeader !== '') {
61 | const links = linkHeader.split(',');
62 | for (const link of links) {
63 | const [url, rel] = link.split(';');
64 | if (rel?.includes('rel="next"')) {
65 | hasNextPage = true;
66 | nextUrl = url?.trim()?.slice(1, -1) ?? null; // Remove < and >
67 | break;
68 | }
69 | }
70 | }
71 |
72 | const data = await response.json();
73 | // Log the response
74 | fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] RESPONSE: ${JSON.stringify(data, null, 2)}\n`);
75 | return { data, hasNextPage, nextUrl };
76 | } catch (error) {
77 | fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] ERROR: ${String(error)}\n`);
78 | throw error;
79 | }
80 | }
81 |
82 | async function fetchAllPages(url: string): Promise {
83 | const allItems: T[] = [];
84 | let currentUrl = url;
85 | let pageCount = 0;
86 |
87 | while (currentUrl !== '') {
88 | pageCount++;
89 | console.log(`Fetching page ${pageCount.toString()} from ${currentUrl}`);
90 | const response = await fetchWithAuth(currentUrl);
91 | console.log(`Got ${Array.isArray(response.data) ? String(response.data.length) : '1'} items`);
92 | if (Array.isArray(response.data)) {
93 | allItems.push(...response.data);
94 | } else {
95 | allItems.push(response.data);
96 | }
97 | currentUrl = response.nextUrl ?? '';
98 | }
99 |
100 | console.log(`Total items fetched: ${allItems.length.toString()}`);
101 | return allItems;
102 | }
103 |
104 | export function isBotComment(comment: GitHubComment): boolean {
105 | return comment.user.login === 'openhands-agent' ||
106 | (comment.user.login === 'github-actions[bot]' && comment.user.type === 'Bot');
107 | }
108 |
109 | export function isStartWorkComment(comment: GitHubComment): boolean {
110 | if (!isBotComment(comment)) return false;
111 | const lowerBody = comment.body.toLowerCase();
112 | return lowerBody.includes('started fixing the') ||
113 | lowerBody.includes('openhands started fixing');
114 | }
115 |
116 | export function isSuccessComment(comment: GitHubComment): boolean {
117 | if (!isBotComment(comment)) {
118 | fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] COMMENT CHECK: Not a bot comment - ${comment.user.login}\n`);
119 | return false;
120 | }
121 | const lowerBody = comment.body.toLowerCase();
122 | const isSuccess = lowerBody.includes('a potential fix has been generated') ||
123 | lowerBody.includes('openhands made the following changes to resolve the issues') ||
124 | lowerBody.includes('successfully fixed') ||
125 | lowerBody.includes('updated pull request');
126 | fs.appendFileSync('github-api.log', `[${new Date().toISOString()}] COMMENT CHECK: Bot comment - Success: ${String(isSuccess)}\nBody: ${comment.body}\n`);
127 | return isSuccess;
128 | }
129 |
130 | export function isFailureComment(comment: GitHubComment): boolean {
131 | if (!isBotComment(comment)) return false;
132 | const lowerBody = comment.body.toLowerCase();
133 | return lowerBody.includes('the workflow to fix this issue encountered an error') ||
134 | lowerBody.includes('openhands failed to create any code changes') ||
135 | lowerBody.includes('an attempt was made to automatically fix this issue, but it was unsuccessful');
136 | }
137 |
138 | export function isPRModificationComment(comment: GitHubComment): boolean {
139 | return isStartWorkComment(comment);
140 | }
141 |
142 | export function isPRModificationSuccessComment(comment: GitHubComment): boolean {
143 | return isSuccessComment(comment);
144 | }
145 |
146 | export function isPRModificationFailureComment(comment: GitHubComment): boolean {
147 | return isFailureComment(comment);
148 | }
149 |
150 | async function checkPRStatus(prUrl: string): Promise {
151 | try {
152 | const response = await fetchWithAuth(prUrl);
153 | const pr = response.data as GitHubPRResponse;
154 |
155 | if (pr?.merged) {
156 | return 'pr_merged';
157 | } else if (pr?.state === 'closed') {
158 | return 'pr_closed';
159 | } else {
160 | return 'pr_open';
161 | }
162 | } catch (error) {
163 | console.error('Error checking PR status:', error);
164 | return 'no_pr';
165 | }
166 | }
167 |
168 | async function processIssueComments(issue: GitHubIssue): Promise {
169 | const activities: Activity[] = [];
170 | const comments = await fetchAllPages(issue.comments_url);
171 |
172 | for (let i = 0; i < comments.length; i++) {
173 | const comment = comments[i];
174 | if (comment && isStartWorkComment(comment)) {
175 | // Look for the next relevant comment to determine success/failure
176 | const nextComments = comments.slice(i + 1);
177 | const successComment = nextComments.find(isSuccessComment);
178 | const failureComment = nextComments.find(isFailureComment);
179 | const resultComment = successComment ?? failureComment;
180 |
181 | if (resultComment !== undefined) {
182 | const timestamp = new Date(resultComment.created_at).toLocaleString();
183 |
184 | // Extract PR URL from success comment if available
185 | let prUrl: string | undefined;
186 | let status: IssueStatus = 'no_pr';
187 |
188 | if (successComment) {
189 | // Try different PR reference formats
190 | let prNumber: string | undefined;
191 |
192 | // Try full PR URL format
193 | const fullUrlMatch = successComment.body.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/);
194 | if (fullUrlMatch) {
195 | prNumber = fullUrlMatch[1];
196 | }
197 |
198 | // Try pull/123 format
199 | if (!prNumber) {
200 | const pullMatch = successComment.body.match(/pull\/(\d+)/);
201 | if (pullMatch) {
202 | prNumber = pullMatch[1];
203 | }
204 | }
205 |
206 | // Try #123 format when it refers to a PR
207 | if (!prNumber) {
208 | const hashMatch = successComment.body.match(/PR #(\d+)/i);
209 | if (hashMatch) {
210 | prNumber = hashMatch[1];
211 | }
212 | }
213 |
214 | if (prNumber) {
215 | prUrl = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/pulls/${prNumber}`;
216 | // Check PR status
217 | status = await checkPRStatus(prUrl);
218 | }
219 | }
220 |
221 | activities.push({
222 | id: `issue-${String(issue.number)}-${String(comment.id)}`,
223 | type: 'issue',
224 | status,
225 | timestamp: resultComment.created_at,
226 | url: resultComment.html_url,
227 | title: `ISSUE ${status} ${timestamp} -- ${issue.title}`,
228 | description: issue.body.slice(0, 500) + (issue.body.length > 500 ? '...' : ''),
229 | prUrl,
230 | });
231 | }
232 | }
233 | }
234 |
235 | return activities;
236 | }
237 |
238 | async function processPRComments(pr: GitHubPR): Promise {
239 | const activities: Activity[] = [];
240 | const comments = await fetchAllPages(pr.comments_url);
241 |
242 | for (let i = 0; i < comments.length; i++) {
243 | const comment = comments[i];
244 | if (comment && isPRModificationComment(comment)) {
245 | // Look for the next relevant comment to determine success/failure
246 | const nextComments = comments.slice(i + 1);
247 | const successComment = nextComments.find(isPRModificationSuccessComment);
248 | const failureComment = nextComments.find(isPRModificationFailureComment);
249 | const resultComment = successComment ?? failureComment;
250 |
251 | if (resultComment !== undefined) {
252 | const status: PRStatus = successComment !== undefined ? 'success' : 'failure';
253 | const timestamp = new Date(resultComment.created_at).toLocaleString();
254 | activities.push({
255 | id: `pr-${String(pr.number)}-${String(comment.id)}`,
256 | type: 'pr',
257 | status,
258 | timestamp: resultComment.created_at,
259 | url: resultComment.html_url,
260 | title: `PR ${status} ${timestamp} -- ${pr.title}`,
261 | description: pr.body ? (pr.body.slice(0, 500) + (pr.body.length > 500 ? '...' : '')) : 'No description provided',
262 | });
263 | }
264 | }
265 | }
266 |
267 | return activities;
268 | }
269 |
270 | // Only run main() if this file is being run directly
271 | if (require.main === module) {
272 | async function main() {
273 | try {
274 | const activities = await fetchBotActivities();
275 | process.stdout.write(JSON.stringify(activities, null, 2));
276 | } catch (error) {
277 | process.stderr.write(String(error) + '\n');
278 | process.exit(1);
279 | }
280 | }
281 | main();
282 | }
283 |
284 | export async function fetchBotActivities(since?: string): Promise {
285 | const startTime = performance.now();
286 | try {
287 | if (!GITHUB_TOKEN || GITHUB_TOKEN === 'placeholder') {
288 | process.stderr.write('Error: GITHUB_TOKEN environment variable is not set or invalid\n');
289 | throw new Error(String('Invalid GITHUB_TOKEN'));
290 | }
291 | console.log('Starting bot activities fetch...');
292 | const activities: Activity[] = [];
293 | const baseUrl = `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}`;
294 |
295 | // Fetch issues and PRs with comments
296 | const params = new URLSearchParams({
297 | state: 'all',
298 | sort: 'updated',
299 | direction: 'desc',
300 | per_page: '100',
301 | });
302 |
303 | if (since !== undefined && since !== '') {
304 | params.append('since', since);
305 | } else {
306 | // Default to last 30 days if no since parameter
307 | const thirtyDaysAgo = new Date();
308 | thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
309 | params.append('since', thirtyDaysAgo.toISOString());
310 | }
311 |
312 | // Fetch issues and PRs
313 | console.log('Fetching issues and PRs...');
314 | const fetchStartTime = performance.now();
315 | const items = await fetchAllPages(`${baseUrl}/issues?${params.toString()}`);
316 | console.log(`Fetched ${String(items.length)} items in ${((performance.now() - fetchStartTime) / 1000).toFixed(2)}s`);
317 |
318 | console.log('Processing items...');
319 | const processStartTime = performance.now();
320 | // Filter items that have comments
321 | const itemsWithComments = items.filter(item => item.comments > 0);
322 | console.log(`Processing ${String(itemsWithComments.length)} items with comments in parallel...`);
323 |
324 | // Process items in parallel
325 | const batchSize = 10; // Process 10 items at a time to avoid rate limiting
326 | const results = [];
327 |
328 | for (let i = 0; i < itemsWithComments.length; i += batchSize) {
329 | const batch = itemsWithComments.slice(i, i + batchSize);
330 | const batchNumber = Math.floor(i/batchSize) + 1;
331 | const totalBatches = Math.ceil(itemsWithComments.length/batchSize);
332 | console.log(`Processing batch ${String(batchNumber)}/${String(totalBatches)}...`);
333 |
334 | const batchResults = await Promise.all(
335 | batch.map(async item => {
336 | if (item.pull_request === undefined) {
337 | // Process regular issues
338 | return processIssueComments(item);
339 | } else {
340 | // Process PRs through the issue comments endpoint to catch all activity
341 | return processPRComments({
342 | number: item.number,
343 | title: item.title,
344 | html_url: item.html_url,
345 | comments_url: item.comments_url,
346 | comments: item.comments,
347 | body: item.body
348 | });
349 | }
350 | })
351 | );
352 |
353 | results.push(...batchResults);
354 | }
355 |
356 | // Flatten results and add to activities
357 | activities.push(...results.flat());
358 |
359 | console.log(`Processed all items in ${((performance.now() - processStartTime) / 1000).toFixed(2)}s`);
360 |
361 | // Sort by timestamp in descending order
362 | console.log('Sorting activities...');
363 | const sortStartTime = performance.now();
364 | const sortedActivities = activities.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime());
365 | console.log(`Sorted ${String(activities.length)} activities in ${((performance.now() - sortStartTime) / 1000).toFixed(2)}s`);
366 |
367 | const totalTime = (performance.now() - startTime) / 1000;
368 | console.log(`Total execution time: ${totalTime.toFixed(2)}s`);
369 |
370 | return sortedActivities;
371 | } catch (error) {
372 | const errorMessage = error instanceof Error ? error.message : String(error);
373 | process.stderr.write('Error fetching bot activities: ' + errorMessage + '\n');
374 | const totalTime = (performance.now() - startTime) / 1000;
375 | process.stderr.write('Total execution time: ' + totalTime.toFixed(2) + 's (failed)\n');
376 | throw new Error(errorMessage);
377 | }
378 | }
--------------------------------------------------------------------------------
/public/cache/bot-activities.json:
--------------------------------------------------------------------------------
1 | {
2 | "activities": [
3 | {
4 | "id": "issue-5423-2531945738",
5 | "type": "issue",
6 | "status": "no_pr",
7 | "timestamp": "2024-12-10T15:12:26Z",
8 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5423#issuecomment-2531962130",
9 | "title": "ISSUE no_pr 12/10/2024, 3:12:26 PM -- [Bug]: When Performing browser operations, output messages are repeated",
10 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nStart a new session and enter the prompt: `What is the area in square miles of the state of California?`\r\nThe response `Let me search for this information.` is repeated twice before the actual answer is received.\r\n\r\n### OpenHands Installation\r\n\r\nDocker command in README\r\n\r\n### OpenHands Version\r\n\r\n0.15.0\r\n\r\n### Operating System\r\n\r\nMacOS\r\n\r\n### Logs, Erro..."
11 | },
12 | {
13 | "id": "issue-5423-2531932774",
14 | "type": "issue",
15 | "status": "no_pr",
16 | "timestamp": "2024-12-10T15:04:35Z",
17 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5423#issuecomment-2531939912",
18 | "title": "ISSUE no_pr 12/10/2024, 3:04:35 PM -- [Bug]: When Performing browser operations, output messages are repeated",
19 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nStart a new session and enter the prompt: `What is the area in square miles of the state of California?`\r\nThe response `Let me search for this information.` is repeated twice before the actual answer is received.\r\n\r\n### OpenHands Installation\r\n\r\nDocker command in README\r\n\r\n### OpenHands Version\r\n\r\n0.15.0\r\n\r\n### Operating System\r\n\r\nMacOS\r\n\r\n### Logs, Erro..."
20 | },
21 | {
22 | "id": "pr-5498-2530407818",
23 | "type": "pr",
24 | "status": "failure",
25 | "timestamp": "2024-12-10T05:12:15Z",
26 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5498#issuecomment-2530416981",
27 | "title": "PR failure 12/10/2024, 5:12:15 AM -- Fix issue #5492: [Bug]: Regression on Anthropic due to empty content hack for Bedrock",
28 | "description": "This pull request fixes #5492.\n\nThe issue has been successfully resolved. The PR addresses the root cause by:\n\n1. Making the empty content removal logic provider-specific, only applying it to Bedrock\n2. Modifying the message serialization to pass the provider information through the pipeline\n3. Adding comprehensive test coverage to verify the behavior works correctly for both Bedrock and Anthropic providers\n\nThe changes directly fix the reported KeyError by ensuring the 'content' key remains pre..."
29 | },
30 | {
31 | "id": "issue-5480-2529235979",
32 | "type": "issue",
33 | "status": "pr_open",
34 | "timestamp": "2024-12-10T04:12:42Z",
35 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5480#issuecomment-2530257505",
36 | "title": "ISSUE pr_open 12/10/2024, 4:12:42 AM -- [Bug]: Cannot recover from \"Agent stuck in loop\"",
37 | "description": "### Is there an existing issue for the same bug?\n\n- [X] I have checked the existing issues.\n\n### Describe the bug and reproduction steps\n\nOnce we stuck in \"agent stuck in loop\", we cannot send subsequent messages to the agent anymore.\r\n\r\n
\r\n\n\n### OpenHands Installation\n\nDocker command in README\n\n### OpenHands Version\n\n_No response_\n\n### Operating System\n\nNone\n\n### Logs, Errors, Scree...",
38 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5500"
39 | },
40 | {
41 | "id": "issue-5480-2530243035",
42 | "type": "issue",
43 | "status": "pr_open",
44 | "timestamp": "2024-12-10T04:12:42Z",
45 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5480#issuecomment-2530257505",
46 | "title": "ISSUE pr_open 12/10/2024, 4:12:42 AM -- [Bug]: Cannot recover from \"Agent stuck in loop\"",
47 | "description": "### Is there an existing issue for the same bug?\n\n- [X] I have checked the existing issues.\n\n### Describe the bug and reproduction steps\n\nOnce we stuck in \"agent stuck in loop\", we cannot send subsequent messages to the agent anymore.\r\n\r\n
\r\n\n\n### OpenHands Installation\n\nDocker command in README\n\n### OpenHands Version\n\n_No response_\n\n### Operating System\n\nNone\n\n### Logs, Errors, Scree...",
48 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5500"
49 | },
50 | {
51 | "id": "issue-5492-2530226892",
52 | "type": "issue",
53 | "status": "pr_open",
54 | "timestamp": "2024-12-10T04:07:25Z",
55 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5492#issuecomment-2530244805",
56 | "title": "ISSUE pr_open 12/10/2024, 4:07:25 AM -- [Bug]: Regression on Anthropic due to empty content hack for Bedrock",
57 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nThis fix https://github.com/All-Hands-AI/OpenHands/pull/4935 to special case empty content string for Bedrock is causing regression with Anthropic tool call that has empty output.\r\n\r\nThis is the bug:\r\n```\r\nmessage.py#_list_serializer\r\n # pop content if it's empty\r\n if not content or (\r\n len(content) == 1\r\n and content[...",
58 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5498"
59 | },
60 | {
61 | "id": "pr-5483-2529725095",
62 | "type": "pr",
63 | "status": "failure",
64 | "timestamp": "2024-12-09T23:28:24Z",
65 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5483#issuecomment-2529777556",
66 | "title": "PR failure 12/9/2024, 11:28:24 PM -- Fix issue #5478: Add color to the line next to \"Ran a XXX Command\" based on return value",
67 | "description": "This pull request fixes #5478.\n\nThe issue has been successfully resolved. The AI implemented a complete solution that addresses the original request to visually indicate command execution success/failure through line colors. Specifically:\n\n1. Added necessary data structures (success property) in both frontend and backend\n2. Implemented color-coding logic:\n - Green for successful commands (exit code 0)\n - Red for failed commands (non-zero exit code)\n - Gray for other messages (default)\n3. M..."
68 | },
69 | {
70 | "id": "pr-5483-2529345378",
71 | "type": "pr",
72 | "status": "failure",
73 | "timestamp": "2024-12-09T20:13:26Z",
74 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5483#issuecomment-2529349069",
75 | "title": "PR failure 12/9/2024, 8:13:26 PM -- Fix issue #5478: Add color to the line next to \"Ran a XXX Command\" based on return value",
76 | "description": "This pull request fixes #5478.\n\nThe issue has been successfully resolved. The AI implemented a complete solution that addresses the original request to visually indicate command execution success/failure through line colors. Specifically:\n\n1. Added necessary data structures (success property) in both frontend and backend\n2. Implemented color-coding logic:\n - Green for successful commands (exit code 0)\n - Red for failed commands (non-zero exit code)\n - Gray for other messages (default)\n3. M..."
77 | },
78 | {
79 | "id": "pr-5484-2529337741",
80 | "type": "pr",
81 | "status": "failure",
82 | "timestamp": "2024-12-09T20:11:41Z",
83 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5484#issuecomment-2529346060",
84 | "title": "PR failure 12/9/2024, 8:11:41 PM -- Remove Beta label from Browser tab",
85 | "description": "This PR removes the Beta label from the Browser tab in the frontend interface.\n\n---\n\nTo run this PR locally, use the following command:\n```\ndocker run -it --rm -p 3000:3000 -v /var/run/docker.sock:/var/run/docker.sock --add-host host.docker.internal:host-gateway -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0df0ea7-nikolaik --name openhands-app-0df0ea7 docker.all-hands.dev/all-hands-ai/openhands:0df0ea7\n```"
86 | },
87 | {
88 | "id": "issue-5478-2529207532",
89 | "type": "issue",
90 | "status": "pr_open",
91 | "timestamp": "2024-12-09T19:44:44Z",
92 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5478#issuecomment-2529253200",
93 | "title": "ISSUE pr_open 12/9/2024, 7:44:44 PM -- Add color to the line next to \"Ran a XXX Command\" based on return value",
94 | "description": "**What problem or use case are you trying to solve?**\r\n\r\nIn the interface, we have \"Ran a Bash Command\" or \"Ran a Jupyter Command\" with a line next to it.\r\n\r\n
\r\n\r\nIt would be nice to know if the command was successful or not.\r\n\r\n**Describe the UX of the solution you'd like**\r\n\r\nWe could change the color of the line depending on the return value. If the c...",
95 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5483"
96 | },
97 | {
98 | "id": "issue-5439-2529212562",
99 | "type": "issue",
100 | "status": "pr_open",
101 | "timestamp": "2024-12-09T19:37:51Z",
102 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5439#issuecomment-2529227186",
103 | "title": "ISSUE pr_open 12/9/2024, 7:37:51 PM -- [Bug]: Scrollbar for the file goes out of screen",
104 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nNot sure if this is because the file is really large but the scroll goes beyond what's the displayed on the screen.\r\n\r\n\r\n\r\n\r\n### OpenHands Installation\r\n\r\nDocker command in README\r\n\r\n### OpenHands Version\r\n\r\nmain\r\n\r\n### Operating System...",
105 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5481"
106 | },
107 | {
108 | "id": "issue-5465-2529206198",
109 | "type": "issue",
110 | "status": "no_pr",
111 | "timestamp": "2024-12-09T19:34:14Z",
112 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5465#issuecomment-2529214825",
113 | "title": "ISSUE no_pr 12/9/2024, 7:34:14 PM -- [Bug]: [Resolver] PR (without a label?) fails workflow",
114 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nI asked openhands-agent to do something, using the macro.\r\nhttps://github.com/All-Hands-AI/OpenHands/pull/5435#issuecomment-2525041512\r\n\r\nAction log:\r\n\r\n> Run if [[ \"\" == \"fix-me-experimental\" ]] ||\r\n if [[ \"\" == \"fix-me-experimental\" ]] ||\r\n ([[ \"issue_comment\" == \"issue_comment\" || \"issue_comment\" == \"pull_request_review_comment\" ]] &&\r\n [[ \"..."
115 | },
116 | {
117 | "id": "issue-5471-2529155965",
118 | "type": "issue",
119 | "status": "pr_merged",
120 | "timestamp": "2024-12-09T19:19:50Z",
121 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5471#issuecomment-2529162588",
122 | "title": "ISSUE pr_merged 12/9/2024, 7:19:50 PM -- Resolver: LLM_MODEL should use \"variable\" instead of \"secret\" ",
123 | "description": "**What problem or use case are you trying to solve?**\nCurrently it's impossible to see in the Resolver logs what LLM was used and it seems unnecessary to hide it. \n\n**Describe the UX of the solution you'd like**\nAny settings that aren't sensitive should use action \"variables\" instead of \"secrets\".\n\n**Do you have thoughts on the technical implementation?**\n\n**Describe alternatives you've considered**\n\n**Additional context**\n",
124 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5477"
125 | },
126 | {
127 | "id": "issue-5456-2525313465",
128 | "type": "issue",
129 | "status": "pr_closed",
130 | "timestamp": "2024-12-07T21:32:49Z",
131 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5456#issuecomment-2525317055",
132 | "title": "ISSUE pr_closed 12/7/2024, 9:32:49 PM -- [Bug]: Too many detected issues in github resolver",
133 | "description": "### Is there an existing issue for the same bug?\n\n- [X] I have checked the existing issues.\n\n### Describe the bug and reproduction steps\n\nThere are too many issues detected in the github resolver. For instance, running on this PR: https://github.com/All-Hands-AI/OpenHands/pull/5449\r\n\r\nFinds many issues that are not mentioned anywhere in the PR:\r\n```\r\n$ poetry run python openhands/resolver/resolve_issue.py --repo all-hands-ai/openhands --issue-type pr --issue-number 5432\r\n16:15:54 - openhands:INF...",
134 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5457"
135 | },
136 | {
137 | "id": "issue-5450-2525287781",
138 | "type": "issue",
139 | "status": "pr_merged",
140 | "timestamp": "2024-12-07T19:37:04Z",
141 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5450#issuecomment-2525288775",
142 | "title": "ISSUE pr_merged 12/7/2024, 7:37:04 PM -- In openhands-resolver.yml, request code review from the person who initiated the workflow",
143 | "description": "**What problem or use case are you trying to solve?**\r\n\r\nopenhands-resolver.yml often results in OpenHands opening a pull request on a repository, but these pull requests can become lost.\r\n\r\n**Describe the UX of the solution you'd like**\r\n\r\nWhen the Open Hands resolver initiates or updates a pull request, it should request review from the person who initiated the GitHub workflow. This will put the pr on the list of the person who initiated the workflow, making it less likely that they will miss ...",
144 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5451"
145 | },
146 | {
147 | "id": "issue-5448-2525284593",
148 | "type": "issue",
149 | "status": "pr_open",
150 | "timestamp": "2024-12-07T19:27:48Z",
151 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5448#issuecomment-2525286456",
152 | "title": "ISSUE pr_open 12/7/2024, 7:27:48 PM -- When using the GitHub resolver on a PR, automatically pipe in failure info",
153 | "description": "**What problem or use case are you trying to solve?**\r\n\r\nOne common use case of the GitHub resolver is to update a PR, and it is also frequent that the pr will have CI actions failing or merge conflicts. If we indicate to the agent that CI actions are failing or there are merge conflicts, we could improve the possibility that it will fix these while making any other changes.\r\n\r\n**Describe the UX of the solution you'd like**\r\n\r\nWhen they get help is over starts working on a PR, we make calls to t...",
154 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5449"
155 | },
156 | {
157 | "id": "pr-5435-2524954159",
158 | "type": "pr",
159 | "status": "failure",
160 | "timestamp": "2024-12-07T08:03:00Z",
161 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5435#issuecomment-2524997119",
162 | "title": "PR failure 12/7/2024, 8:03:00 AM -- Fix issue #4706: [Feature]: Give descriptive name to downloaded zip file",
163 | "description": "**End-user friendly description of the problem this fixes or functionality that this introduces**\r\n\r\n- [ ] Include this change in the Release Notes. If checked, you must provide an **end-user friendly** description for your change below\r\n\r\n---\r\n**Give a summary of what the PR does, explaining any non-trivial design decisions**\r\n\r\nThe issue has not been successfully resolved. The last message shows that the test command failed with exit code 1, indicating that the unit tests are failing. This sug..."
164 | },
165 | {
166 | "id": "pr-5435-2524996237",
167 | "type": "pr",
168 | "status": "failure",
169 | "timestamp": "2024-12-07T08:03:00Z",
170 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5435#issuecomment-2524997119",
171 | "title": "PR failure 12/7/2024, 8:03:00 AM -- Fix issue #4706: [Feature]: Give descriptive name to downloaded zip file",
172 | "description": "**End-user friendly description of the problem this fixes or functionality that this introduces**\r\n\r\n- [ ] Include this change in the Release Notes. If checked, you must provide an **end-user friendly** description for your change below\r\n\r\n---\r\n**Give a summary of what the PR does, explaining any non-trivial design decisions**\r\n\r\nThe issue has not been successfully resolved. The last message shows that the test command failed with exit code 1, indicating that the unit tests are failing. This sug..."
173 | },
174 | {
175 | "id": "issue-5445-2524954999",
176 | "type": "issue",
177 | "status": "pr_open",
178 | "timestamp": "2024-12-07T06:01:26Z",
179 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5445#issuecomment-2524956314",
180 | "title": "ISSUE pr_open 12/7/2024, 6:01:26 AM -- [Bug]: Assigning label on PR doesn't kickstart resolver",
181 | "description": "### Is there an existing issue for the same bug?\n\n- [X] I have checked the existing issues.\n\n### Describe the bug and reproduction steps\n\nTried adding `fix-me`/`fix-me-experimental` label to #5342 but it didn't kickstart the resolver\n\n### OpenHands Installation\n\nOther\n\n### OpenHands Version\n\nlatest,main\n\n### Operating System\n\nNone\n\n### Logs, Errors, Screenshots, and Additional Context\n\n_No response_",
182 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5446"
183 | },
184 | {
185 | "id": "issue-4706-2522125016",
186 | "type": "issue",
187 | "status": "no_pr",
188 | "timestamp": "2024-12-06T16:29:03Z",
189 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/4706#issuecomment-2523682063",
190 | "title": "ISSUE no_pr 12/6/2024, 4:29:03 PM -- [Feature]: Give descriptive name to downloaded zip file",
191 | "description": "**What problem or use case are you trying to solve?**\r\n\r\nCurrently, when we use the frontend feature to download a zip file, it is always called `workspace.zip`. If you do this over and over again, you get a lot of files called `workspace.zip` and it's a little bit annoying to keep track of which is which.\r\n\r\n**Describe the UX of the solution you'd like**\r\n\r\nIt would be nice if this file was given a descriptive name.\r\n\r\n**Do you have thoughts on the technical implementation?**\r\n\r\nWhen the \"downl..."
192 | },
193 | {
194 | "id": "issue-4706-2523586809",
195 | "type": "issue",
196 | "status": "no_pr",
197 | "timestamp": "2024-12-06T16:29:03Z",
198 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/4706#issuecomment-2523682063",
199 | "title": "ISSUE no_pr 12/6/2024, 4:29:03 PM -- [Feature]: Give descriptive name to downloaded zip file",
200 | "description": "**What problem or use case are you trying to solve?**\r\n\r\nCurrently, when we use the frontend feature to download a zip file, it is always called `workspace.zip`. If you do this over and over again, you get a lot of files called `workspace.zip` and it's a little bit annoying to keep track of which is which.\r\n\r\n**Describe the UX of the solution you'd like**\r\n\r\nIt would be nice if this file was given a descriptive name.\r\n\r\n**Do you have thoughts on the technical implementation?**\r\n\r\nWhen the \"downl..."
201 | },
202 | {
203 | "id": "issue-5359-2516062933",
204 | "type": "issue",
205 | "status": "no_pr",
206 | "timestamp": "2024-12-04T03:58:51Z",
207 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5359#issuecomment-2516134796",
208 | "title": "ISSUE no_pr 12/4/2024, 3:58:51 AM -- [Resolver] The LLM uses a non-optimal way to install openhands",
209 | "description": "**What problem or use case are you trying to solve?**\r\n\r\nCheck the openhands resolver functionality.\r\nWhen the openhands resolver is running:\r\n\r\nI see these days the LLM:\r\n- trying to run pytest\r\n- finds out it's not installed, so it runs `poetry install`\r\n- it has in the prompt the info that it should set up the project with `make build`, but that doesn't seem to happen\r\n\r\nThe problem is that `poetry install` installs all optional dependencies like `torch`, `nvidia-cuda*` etc.\r\n\r\nBy default, `m..."
210 | },
211 | {
212 | "id": "issue-5359-2516126395",
213 | "type": "issue",
214 | "status": "no_pr",
215 | "timestamp": "2024-12-04T03:58:51Z",
216 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5359#issuecomment-2516134796",
217 | "title": "ISSUE no_pr 12/4/2024, 3:58:51 AM -- [Resolver] The LLM uses a non-optimal way to install openhands",
218 | "description": "**What problem or use case are you trying to solve?**\r\n\r\nCheck the openhands resolver functionality.\r\nWhen the openhands resolver is running:\r\n\r\nI see these days the LLM:\r\n- trying to run pytest\r\n- finds out it's not installed, so it runs `poetry install`\r\n- it has in the prompt the info that it should set up the project with `make build`, but that doesn't seem to happen\r\n\r\nThe problem is that `poetry install` installs all optional dependencies like `torch`, `nvidia-cuda*` etc.\r\n\r\nBy default, `m..."
219 | },
220 | {
221 | "id": "issue-5383-2516059755",
222 | "type": "issue",
223 | "status": "pr_merged",
224 | "timestamp": "2024-12-04T03:24:40Z",
225 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5383#issuecomment-2516089046",
226 | "title": "ISSUE pr_merged 12/4/2024, 3:24:40 AM -- [Bug]: LLM Cost is added to the `metrics` twice",
227 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nIn the file `openhands/llm/llm.py`, when the LLM completion is invoked, the method `_completion_cost` will be called twice.\r\n\r\nOne is when preparing to store the log data for this completion:\r\nhttps://github.com/All-Hands-AI/OpenHands/blob/2f11634ccaeb3aa38893a994d53ff280bbf485d0/openhands/llm/llm.py#L231-L238\r\n\r\nThe other one is when calling the final c...",
228 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5396"
229 | },
230 | {
231 | "id": "issue-5383-2516080771",
232 | "type": "issue",
233 | "status": "pr_merged",
234 | "timestamp": "2024-12-04T03:24:40Z",
235 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5383#issuecomment-2516089046",
236 | "title": "ISSUE pr_merged 12/4/2024, 3:24:40 AM -- [Bug]: LLM Cost is added to the `metrics` twice",
237 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nIn the file `openhands/llm/llm.py`, when the LLM completion is invoked, the method `_completion_cost` will be called twice.\r\n\r\nOne is when preparing to store the log data for this completion:\r\nhttps://github.com/All-Hands-AI/OpenHands/blob/2f11634ccaeb3aa38893a994d53ff280bbf485d0/openhands/llm/llm.py#L231-L238\r\n\r\nThe other one is when calling the final c...",
238 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5396"
239 | },
240 | {
241 | "id": "pr-5308-2514893031",
242 | "type": "pr",
243 | "status": "failure",
244 | "timestamp": "2024-12-03T15:40:06Z",
245 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5308#issuecomment-2514913053",
246 | "title": "PR failure 12/3/2024, 3:40:06 PM -- Fix issue #5112: [Bug]: \"Push to GitHub\" shows up even if there's no repo connected",
247 | "description": "This pull request fixes #5112.\n\nThe issue has been successfully resolved. The PR addresses the core problem by fixing the logic for when the \"Push to GitHub\" button should be displayed. The key changes were:\n\n1. Adding a new `hasConnectedRepo` prop that specifically checks for an actual connected repository (via `githubData`)\n2. Modifying the display logic to require both GitHub connection AND a connected repository\n3. This ensures the button only appears when functionally useful (when there's a..."
248 | },
249 | {
250 | "id": "pr-5284-2501338361",
251 | "type": "pr",
252 | "status": "failure",
253 | "timestamp": "2024-12-01T01:19:17Z",
254 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5284#issuecomment-2509509888",
255 | "title": "PR failure 12/1/2024, 1:19:17 AM -- feat: Add LocalRuntime and rename EventStreamRuntime to DockerRuntime",
256 | "description": "This PR adds a new LocalRuntime implementation and renames EventStreamRuntime to LocalDockerRuntime for better clarity.\r\n\r\n### Changes\r\n\r\n- Add new LocalRuntime implementation that runs action_execution_server directly on the host machine\r\n- Rename EventStreamRuntime to LocalDockerRuntime for better clarity\r\n- Move runtime implementations to dedicated directories (local/ and docker/)\r\n- Update documentation to reflect runtime changes and add LocalRuntime description\r\n\r\n### Benefits\r\n\r\n- Provides..."
257 | },
258 | {
259 | "id": "pr-5284-2507055044",
260 | "type": "pr",
261 | "status": "failure",
262 | "timestamp": "2024-12-01T01:19:17Z",
263 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5284#issuecomment-2509509888",
264 | "title": "PR failure 12/1/2024, 1:19:17 AM -- feat: Add LocalRuntime and rename EventStreamRuntime to DockerRuntime",
265 | "description": "This PR adds a new LocalRuntime implementation and renames EventStreamRuntime to LocalDockerRuntime for better clarity.\r\n\r\n### Changes\r\n\r\n- Add new LocalRuntime implementation that runs action_execution_server directly on the host machine\r\n- Rename EventStreamRuntime to LocalDockerRuntime for better clarity\r\n- Move runtime implementations to dedicated directories (local/ and docker/)\r\n- Update documentation to reflect runtime changes and add LocalRuntime description\r\n\r\n### Benefits\r\n\r\n- Provides..."
266 | },
267 | {
268 | "id": "pr-5284-2508709941",
269 | "type": "pr",
270 | "status": "failure",
271 | "timestamp": "2024-12-01T01:19:17Z",
272 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5284#issuecomment-2509509888",
273 | "title": "PR failure 12/1/2024, 1:19:17 AM -- feat: Add LocalRuntime and rename EventStreamRuntime to DockerRuntime",
274 | "description": "This PR adds a new LocalRuntime implementation and renames EventStreamRuntime to LocalDockerRuntime for better clarity.\r\n\r\n### Changes\r\n\r\n- Add new LocalRuntime implementation that runs action_execution_server directly on the host machine\r\n- Rename EventStreamRuntime to LocalDockerRuntime for better clarity\r\n- Move runtime implementations to dedicated directories (local/ and docker/)\r\n- Update documentation to reflect runtime changes and add LocalRuntime description\r\n\r\n### Benefits\r\n\r\n- Provides..."
275 | },
276 | {
277 | "id": "pr-5284-2509025424",
278 | "type": "pr",
279 | "status": "failure",
280 | "timestamp": "2024-12-01T01:19:17Z",
281 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5284#issuecomment-2509509888",
282 | "title": "PR failure 12/1/2024, 1:19:17 AM -- feat: Add LocalRuntime and rename EventStreamRuntime to DockerRuntime",
283 | "description": "This PR adds a new LocalRuntime implementation and renames EventStreamRuntime to LocalDockerRuntime for better clarity.\r\n\r\n### Changes\r\n\r\n- Add new LocalRuntime implementation that runs action_execution_server directly on the host machine\r\n- Rename EventStreamRuntime to LocalDockerRuntime for better clarity\r\n- Move runtime implementations to dedicated directories (local/ and docker/)\r\n- Update documentation to reflect runtime changes and add LocalRuntime description\r\n\r\n### Benefits\r\n\r\n- Provides..."
284 | },
285 | {
286 | "id": "pr-5284-2509489022",
287 | "type": "pr",
288 | "status": "failure",
289 | "timestamp": "2024-12-01T01:19:17Z",
290 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5284#issuecomment-2509509888",
291 | "title": "PR failure 12/1/2024, 1:19:17 AM -- feat: Add LocalRuntime and rename EventStreamRuntime to DockerRuntime",
292 | "description": "This PR adds a new LocalRuntime implementation and renames EventStreamRuntime to LocalDockerRuntime for better clarity.\r\n\r\n### Changes\r\n\r\n- Add new LocalRuntime implementation that runs action_execution_server directly on the host machine\r\n- Rename EventStreamRuntime to LocalDockerRuntime for better clarity\r\n- Move runtime implementations to dedicated directories (local/ and docker/)\r\n- Update documentation to reflect runtime changes and add LocalRuntime description\r\n\r\n### Benefits\r\n\r\n- Provides..."
293 | },
294 | {
295 | "id": "pr-5332-2509492299",
296 | "type": "pr",
297 | "status": "failure",
298 | "timestamp": "2024-12-01T00:42:28Z",
299 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5332#issuecomment-2509498863",
300 | "title": "PR failure 12/1/2024, 12:42:28 AM -- Fix issue #4864: [Bug]: make start-backend results in NotImplementedError: Non-relative patterns are unsupported",
301 | "description": "This pull request fixes #4864.\n\nThe issue has been successfully resolved with a simple but effective fix. The original error was caused by using absolute paths with uvicorn's `--reload-exclude` option, which isn't supported by Python's `pathlib.glob()`. \n\nThe solution implemented was to modify the Makefile's `start-backend` target to use a relative path `\"./workspace\"` instead of `$(shell pwd)/workspace`. This change resolved the `NotImplementedError` that was preventing the backend from startin..."
302 | },
303 | {
304 | "id": "issue-5112-2504963369",
305 | "type": "issue",
306 | "status": "pr_open",
307 | "timestamp": "2024-11-27T23:19:45Z",
308 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5112#issuecomment-2504967807",
309 | "title": "ISSUE pr_open 11/27/2024, 11:19:45 PM -- [Bug]: \"Push to GitHub\" shows up even if there's no repo connected",
310 | "description": "### Is there an existing issue for the same bug?\n\n- [X] I have checked the existing issues.\n\n### Describe the bug and reproduction steps\n\n
\r\n\r\nRepro:\r\n* start a new project from scratch\r\n* push to github appears\r\n\r\nMy guess is it currently shows up if you're logged into GitHub, rather than if there's a repo connected to the current project. The latter is...",
311 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5308"
312 | },
313 | {
314 | "id": "issue-5234-2502452335",
315 | "type": "issue",
316 | "status": "no_pr",
317 | "timestamp": "2024-11-27T01:46:40Z",
318 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5234#issuecomment-2502467549",
319 | "title": "ISSUE no_pr 11/27/2024, 1:46:40 AM -- [Bug]: [Resolver] Error when parsing the JSON response on success",
320 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nThis seems to happen all the time: (I think? I don't see a successful comment even if the agent did solve the task, please let me know if I'm doing something wrong, I'm not too familiar with the resolver ❤️ )\r\n\r\nNOTE: the issue is that success_explanation fails parsing as JSON.\r\n\r\n```\r\n06:13:25 - openhands:ERROR: resolve_issue.py:260 - Failed to parse su..."
321 | },
322 | {
323 | "id": "issue-2947-2496600387",
324 | "type": "issue",
325 | "status": "no_pr",
326 | "timestamp": "2024-11-25T03:43:52Z",
327 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/2947#issuecomment-2496615880",
328 | "title": "ISSUE no_pr 11/25/2024, 3:43:52 AM -- Feat: make use of litellm's response \"usage\" data",
329 | "description": "**What problem or use case are you trying to solve?**\r\n\r\n**TASK**\r\n\r\nWe want to enhance our get_token_count() implementation in llm.py, to take advantage of the token counts we are provided from our dependencies, if available, and only fallback to count them if they are not already available.\r\n\r\nRead the llm.py file. It uses the litellm library, and you can find some code that uses the Usage object from litellm, too.\r\n\r\nThis `usage` data, when available, provides live token counts. It will be av..."
330 | },
331 | {
332 | "id": "issue-5015-2496303049",
333 | "type": "issue",
334 | "status": "no_pr",
335 | "timestamp": "2024-11-24T23:02:07Z",
336 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5015#issuecomment-2496306399",
337 | "title": "ISSUE no_pr 11/24/2024, 11:02:07 PM -- [Bug]: Headless mode awaits for requested user feedback without showing any text for what that feedback should be",
338 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nCommand to run:\r\n\r\n```\r\ndocker run -it \\\r\n --pull=always \\\r\n -e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.13-nikolaik \\\r\n -e SANDBOX_USER_ID=$(id -u) \\\r\n -e WORKSPACE_MOUNT_PATH=$(pwd) \\\r\n -e LLM_API_KEY=*** \\\r\n -e LLM_MODEL=\"anthropic/claude-3-5-sonnet-20241022\" \\\r\n -v $WORKSPACE_BASE:/opt/workspace..."
339 | },
340 | {
341 | "id": "issue-5154-2495788641",
342 | "type": "issue",
343 | "status": "pr_closed",
344 | "timestamp": "2024-11-24T04:02:33Z",
345 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5154#issuecomment-2495791202",
346 | "title": "ISSUE pr_closed 11/24/2024, 4:02:33 AM -- [Bug]: FinishTool doesn't have a tool response",
347 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nReported [on slack](https://openhands-ai.slack.com/archives/C06R25BT5B2/p1732130085002229). Regression from function calling.\r\n\r\nFinishAction with `source=agent` is not the last thing that happens when running with UI or restoring session in CLI, but our backend doesn't give it a tool response. CodeAct \"forgets\" it, which makes other things not play nice...",
348 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5231"
349 | },
350 | {
351 | "id": "pr-5226-2495596128",
352 | "type": "pr",
353 | "status": "failure",
354 | "timestamp": "2024-11-23T18:36:05Z",
355 | "url": "https://github.com/All-Hands-AI/OpenHands/pull/5226#issuecomment-2495609874",
356 | "title": "PR failure 11/23/2024, 6:36:05 PM -- Fix issue #5186: [Bug]: Fix up inline code styles in chat window",
357 | "description": "This pull request fixes #5186.\n\nThe issue has been successfully resolved. The AI agent made specific improvements to the inline code styling that directly address the reported concerns about font size and visual appearance:\n\n1. Font size was adjusted upward (from 85% to 90%) to address the sizing concern\n2. Background and text colors were modified using appropriate theme variables to improve visibility and contrast\n3. Added a border to make code blocks more distinct\n4. Fine-tuned the border radi..."
358 | },
359 | {
360 | "id": "issue-5229-2495521927",
361 | "type": "issue",
362 | "status": "no_pr",
363 | "timestamp": "2024-11-23T15:59:49Z",
364 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5229#issuecomment-2495522602",
365 | "title": "ISSUE no_pr 11/23/2024, 3:59:49 PM -- [Documentation]: Micro-agents",
366 | "description": "**What problem or use case are you trying to solve?**\r\n\r\nCurrently in the `openhands/agenthub/codeact_agent` directory, we have an implementation of micro agents, but this is not documented.\r\n\r\nTo do so, we can:\r\n1. read the implementation of codeact agent\r\n2. read an example microagent in `openhands/agenthub/codeact_agent/micro/github.md`\r\n3. add documentation to `openhands/agenthub/codeact_agent/README.md`\r\n"
367 | },
368 | {
369 | "id": "issue-5195-2495495143",
370 | "type": "issue",
371 | "status": "pr_closed",
372 | "timestamp": "2024-11-23T14:25:56Z",
373 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5195#issuecomment-2495496472",
374 | "title": "ISSUE pr_closed 11/23/2024, 2:25:56 PM -- [Resolver]: Adding unit tests to Github workflows",
375 | "description": "**Summary**\r\nI would like to add unit tests for Github workflows for `openhands-resolver`.\r\n\r\n**Motivation**\r\nThe workflow definitions are the entry points for `openhands-resolver`, making it the largest potential single point of failure. Currently making changes to the workflows needs to be manually tested in a separate repo. We've added many ways to trigger the workflows, so testing all of them manually tends to be tedious and often error prone. A way to do tests (at least a fraction of them) ...",
376 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5227"
377 | },
378 | {
379 | "id": "issue-5186-2495495630",
380 | "type": "issue",
381 | "status": "pr_merged",
382 | "timestamp": "2024-11-23T14:25:51Z",
383 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5186#issuecomment-2495496450",
384 | "title": "ISSUE pr_merged 11/23/2024, 2:25:51 PM -- [Bug]: Fix up inline code styles in chat window",
385 | "description": "### Is there an existing issue for the same bug?\n\n- [X] I have checked the existing issues.\n\n### Describe the bug and reproduction steps\n\nThe font size for the code is a little bigger, and the spaces feel off. We should maybe change text color and background color too, to make it more apparent\r\n\r\n
\r\n\n\n### OpenHands Installation\n\napp.all-hands.dev\n\n### Op...",
386 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5226"
387 | },
388 | {
389 | "id": "issue-5162-2489951095",
390 | "type": "issue",
391 | "status": "no_pr",
392 | "timestamp": "2024-11-21T02:46:16Z",
393 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5162#issuecomment-2489953661",
394 | "title": "ISSUE no_pr 11/21/2024, 2:46:16 AM -- docs: Improve GitHub token setup documentation in UI guide",
395 | "description": "The current documentation about GitHub token setup in the GUI mode guide is minimal and could be improved to help users better understand the process.\r\n\r\nNeeded improvements:\r\n\r\n1. Add step-by-step instructions for setting up a GitHub token locally:\r\n - How to access the token settings in the UI\r\n - Required token scopes and permissions\r\n - How to enter and save the token\r\n\r\n2. Add information about organizational token policies:\r\n - Note that organizational repositories may require addi..."
396 | },
397 | {
398 | "id": "issue-5112-2484768514",
399 | "type": "issue",
400 | "status": "pr_closed",
401 | "timestamp": "2024-11-19T06:03:50Z",
402 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/5112#issuecomment-2484777977",
403 | "title": "ISSUE pr_closed 11/19/2024, 6:03:50 AM -- [Bug]: \"Push to GitHub\" shows up even if there's no repo connected",
404 | "description": "### Is there an existing issue for the same bug?\n\n- [X] I have checked the existing issues.\n\n### Describe the bug and reproduction steps\n\n
\r\n\r\nRepro:\r\n* start a new project from scratch\r\n* push to github appears\r\n\r\nMy guess is it currently shows up if you're logged into GitHub, rather than if there's a repo connected to the current project. The latter is...",
405 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/5118"
406 | },
407 | {
408 | "id": "issue-4828-2462874019",
409 | "type": "issue",
410 | "status": "pr_closed",
411 | "timestamp": "2024-11-07T17:58:14Z",
412 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/4828#issuecomment-2462891889",
413 | "title": "ISSUE pr_closed 11/7/2024, 5:58:14 PM -- [Bug]: Chat interface empty state flickers for a brief moment until messages load",
414 | "description": "### Is there an existing issue for the same bug?\n\n- [X] I have checked the existing issues.\n\n### Describe the bug and reproduction steps\n\nIf there are messages that are yet to be received by the socket, the empty state UI is shown for a brief moment, resulting in an unpleasant flicker\n\n### OpenHands Installation\n\nDocker command in README\n\n### OpenHands Version\n\nmain\n\n### Operating System\n\nNone\n\n### Logs, Errors, Screenshots, and Additional Context\n\nWe should probably wait until socket is green",
415 | "prUrl": "https://api.github.com/repos/All-Hands-AI/OpenHands/pulls/4831"
416 | },
417 | {
418 | "id": "issue-4809-2461146053",
419 | "type": "issue",
420 | "status": "no_pr",
421 | "timestamp": "2024-11-07T01:50:59Z",
422 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/4809#issuecomment-2461152418",
423 | "title": "ISSUE no_pr 11/7/2024, 1:50:59 AM -- [Bug]: Model does not support image upload when using `litellm_proxy/`",
424 | "description": "### Is there an existing issue for the same bug?\r\n\r\n- [X] I have checked the existing issues.\r\n\r\n### Describe the bug and reproduction steps\r\n\r\nI got the following error when using model `litellm_proxy/claude-3-5-sonnet-20241022` through a LiteLLM proxy. It supposed to support vision inputs.\r\n\r\n
\r\n\r\nIn L345-L348 openhands/llm/llm.py, maybe we should also check for `litellm.support_vi..."
425 | },
426 | {
427 | "id": "issue-4769-2458390348",
428 | "type": "issue",
429 | "status": "no_pr",
430 | "timestamp": "2024-11-05T23:24:47Z",
431 | "url": "https://github.com/All-Hands-AI/OpenHands/issues/4769#issuecomment-2458397249",
432 | "title": "ISSUE no_pr 11/5/2024, 11:24:47 PM -- [Bug]: Markdown numbering is rendered weird",
433 | "description": "### Is there an existing issue for the same bug?\n\n- [X] I have checked the existing issues.\n\n### Describe the bug and reproduction steps\n\nThe agent is likely sending separate messages with \"1.\" \"2.\" \"3.\"\r\n\r\nMarkdown is weird in that it will render any number as \"1.\", i.e.\r\n\r\n```md\r\n42.\r\n86.\r\n93.\r\n```\r\n\r\nwill render as\r\n```\r\n1.\r\n2.\r\n3.\r\n```\r\n\r\nI bet we can turn this behavior off\r\n\r\n