Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions packages/cli/src/__tests__/scripts/core-flow-smoke.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,12 @@ describe('core flow smoke tests (bash scripts)', () => {
{ encoding: 'utf-8', mode: 0o755 },
);

fs.writeFileSync(
path.join(fakeBin, 'claude'),
'#!/usr/bin/env bash\nexit 0\n',
{ encoding: 'utf-8', mode: 0o755 },
);

const result = runScript(reviewerScript, projectDir, {
PATH: `${fakeBin}:${process.env.PATH}`,
NW_PROVIDER_CMD: 'claude',
Expand Down Expand Up @@ -1596,6 +1602,12 @@ describe('core flow smoke tests (bash scripts)', () => {
{ encoding: 'utf-8', mode: 0o755 },
);

fs.writeFileSync(
path.join(fakeBin, 'claude'),
'#!/usr/bin/env bash\nexit 0\n',
{ encoding: 'utf-8', mode: 0o755 },
);

const result = runScript(reviewerScript, projectDir, {
PATH: `${fakeBin}:${process.env.PATH}`,
NW_PROVIDER_CMD: 'claude',
Expand Down Expand Up @@ -1748,6 +1760,12 @@ describe('core flow smoke tests (bash scripts)', () => {
{ encoding: 'utf-8', mode: 0o755 },
);

fs.writeFileSync(
path.join(fakeBin, 'claude'),
'#!/usr/bin/env bash\nexit 0\n',
{ encoding: 'utf-8', mode: 0o755 },
);

const result = runScript(reviewerScript, projectDir, {
PATH: `${fakeBin}:${process.env.PATH}`,
NW_PROVIDER_CMD: 'claude',
Expand Down
22 changes: 14 additions & 8 deletions packages/cli/src/commands/review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,8 +166,7 @@ export function postReadyForHumanReviewComment(
finalScore: number | undefined,
cwd: string,
): void {
const scoreNote =
finalScore !== undefined ? ` (score: ${finalScore}/100)` : '';
const scoreNote = finalScore !== undefined ? ` (score: ${finalScore}/100)` : '';
const body =
`## ✅ Ready for Human Review\n\n` +
`Night Watch has reviewed this PR${scoreNote} and found no issues requiring automated fixes.\n\n` +
Expand Down Expand Up @@ -520,12 +519,16 @@ export function reviewCommand(program: Command): void {
const reviewedPrNumbers = parseReviewedPrNumbers(scriptResult?.data.prs);
const noChangesPrNumbers = parseReviewedPrNumbers(scriptResult?.data.no_changes_prs);
const fallbackPrNumber = fallbackPrDetails?.number;
let primaryPrNumbers: number[];
if (reviewedPrNumbers.length > 0) {
primaryPrNumbers = reviewedPrNumbers;
} else if (fallbackPrNumber !== undefined) {
primaryPrNumbers = [fallbackPrNumber];
} else {
primaryPrNumbers = [];
}
const notificationTargets = buildReviewNotificationTargets(
reviewedPrNumbers.length > 0
? reviewedPrNumbers
: fallbackPrNumber !== undefined
? [fallbackPrNumber]
: [],
primaryPrNumbers,
noChangesPrNumbers,
legacyNoChangesNeeded,
);
Expand Down Expand Up @@ -567,7 +570,10 @@ export function reviewCommand(program: Command): void {
event: reviewEvent,
projectName: path.basename(projectDir),
exitCode,
provider: formatProviderDisplay(envVars.NW_PROVIDER_CMD, envVars.NW_PROVIDER_LABEL),
provider: formatProviderDisplay(
envVars.NW_PROVIDER_CMD,
envVars.NW_PROVIDER_LABEL,
),
prUrl: prDetails?.url,
prTitle: prDetails?.title,
prBody: prDetails?.body,
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
DEFAULT_ANALYTICS,
DEFAULT_AUDIT,
DEFAULT_AUTO_MERGE,
DEFAULT_MERGER,
DEFAULT_AUTO_MERGE_METHOD,
DEFAULT_BOARD_PROVIDER,
DEFAULT_BRANCH_PATTERNS,
Expand All @@ -36,6 +35,7 @@ import {
DEFAULT_MAX_LOG_SIZE,
DEFAULT_MAX_RETRIES,
DEFAULT_MAX_RUNTIME,
DEFAULT_MERGER,
DEFAULT_MIN_REVIEW_SCORE,
DEFAULT_NOTIFICATIONS,
DEFAULT_PRD_DIR,
Expand Down Expand Up @@ -220,7 +220,9 @@ function mergeConfigs(
merged.merger = {
...merged.merger,
enabled: true,
mergeMethod: (merged as unknown as Record<string, unknown>).autoMergeMethod as IMergerConfig['mergeMethod'] ?? 'squash',
mergeMethod:
((merged as unknown as Record<string, unknown>)
.autoMergeMethod as IMergerConfig['mergeMethod']) ?? 'squash',
};
}

Expand Down
145 changes: 145 additions & 0 deletions web/components/scheduling/QueueTab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React from 'react';
import { AlertCircle } from 'lucide-react';
import Card from '../ui/Card.js';
import type { IQueueAnalytics, IQueueStatus } from '../../api.js';
import ProviderLanesChart from './ProviderLanesChart.js';
import ProviderBucketSummary from './ProviderBucketSummary.js';
import RecentRunsChart from './RecentRunsChart.js';

interface IQueueTabProps {
queueStatus: IQueueStatus | null;
queueAnalytics: IQueueAnalytics | null;
queueStatusError: Error | null;
queueAnalyticsError: Error | null;
}

const QueueTab: React.FC<IQueueTabProps> = ({
queueStatus,
queueAnalytics,
queueStatusError,
queueAnalyticsError,
}) => {
return (
<div className="space-y-6">
{/* Queue Overview Card */}
<Card className="p-6">
<h3 className="text-lg font-semibold text-slate-200 mb-4">Queue Overview</h3>
{queueStatusError ? (
<div className="flex items-center gap-2 text-red-400 py-4">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">Failed to load queue status</span>
</div>
) : (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="bg-slate-950/40 rounded-lg p-4 border border-slate-800">
<div className="text-xs uppercase tracking-wide text-slate-500 mb-1">Running</div>
<div className="text-2xl font-bold text-green-400">
{queueStatus?.running ? 1 : 0}
</div>
{queueStatus?.running && (
<div className="text-xs text-slate-400 mt-1 truncate" title={queueStatus.running.projectName}>
{queueStatus.running.jobType} · {queueStatus.running.projectName}
</div>
)}
</div>
<div className="bg-slate-950/40 rounded-lg p-4 border border-slate-800">
<div className="text-xs uppercase tracking-wide text-slate-500 mb-1">Pending</div>
<div className="text-2xl font-bold text-blue-400">
{queueStatus?.pending.total ?? 0}
</div>
</div>
<div className="bg-slate-950/40 rounded-lg p-4 border border-slate-800">
<div className="text-xs uppercase tracking-wide text-slate-500 mb-1">Avg Wait</div>
<div className="text-2xl font-bold text-slate-200">
{queueStatus?.averageWaitSeconds != null
? `${Math.floor(queueStatus.averageWaitSeconds / 60)}m`
: '—'}
</div>
</div>
<div className="bg-slate-950/40 rounded-lg p-4 border border-slate-800">
<div className="text-xs uppercase tracking-wide text-slate-500 mb-1">Oldest Pending</div>
<div className="text-2xl font-bold text-slate-200">
{queueStatus?.oldestPendingAge != null
? `${Math.floor(queueStatus.oldestPendingAge / 60)}m`
: '—'}
</div>
</div>
</div>
)}
</Card>

{/* Provider Lanes */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-slate-200">Provider Lanes</h3>
<p className="text-xs text-slate-500 mt-0.5">
Running and pending jobs grouped by provider bucket
</p>
</div>
</div>
{queueStatusError ? (
<div className="flex items-center gap-2 text-red-400 py-2">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">Failed to load queue status</span>
</div>
) : queueStatus ? (
<ProviderLanesChart status={queueStatus} />
) : (
<div className="text-sm text-slate-500 py-2">Loading queue status...</div>
)}
</Card>

{/* Provider Bucket Summary */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-slate-200">Provider Buckets</h3>
<p className="text-xs text-slate-500 mt-0.5">
Running and pending counts per provider bucket
</p>
</div>
</div>
{queueAnalyticsError ? (
<div className="flex items-center gap-2 text-red-400 py-2">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">Failed to load analytics</span>
</div>
) : queueAnalytics ? (
<ProviderBucketSummary analytics={queueAnalytics} />
) : (
<div className="text-sm text-slate-500 py-2">Loading analytics...</div>
)}
</Card>

{/* Recent Runs */}
<Card className="p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="text-lg font-semibold text-slate-200">Recent Runs</h3>
<p className="text-xs text-slate-500 mt-0.5">
Last 24 hours of job executions
</p>
</div>
{queueAnalytics?.averageWaitSeconds != null && (
<div className="text-xs text-slate-500">
Avg wait: {Math.floor(queueAnalytics.averageWaitSeconds / 60)}m
</div>
)}
</div>
{queueAnalyticsError ? (
<div className="flex items-center gap-2 text-red-400 py-2">
<AlertCircle className="h-4 w-4" />
<span className="text-sm">Failed to load analytics</span>
</div>
) : queueAnalytics ? (
<RecentRunsChart analytics={queueAnalytics} />
) : (
<div className="text-sm text-slate-500 py-2">Loading analytics...</div>
)}
</Card>
</div>
);
};

export default QueueTab;
56 changes: 50 additions & 6 deletions web/pages/Scheduling.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Select from '../components/ui/Select';
import Switch from '../components/ui/Switch';
import Tabs from '../components/ui/Tabs';
import ScheduleTimeline from '../components/scheduling/ScheduleTimeline.js';
import QueueTab from '../components/scheduling/QueueTab.js';
import { useStore } from '../store/useStore';
import type { INightWatchConfig, IQueueAnalytics, IQueueStatus, QueueMode } from '../api';
import {
Expand Down Expand Up @@ -94,6 +95,8 @@ const Scheduling: React.FC = () => {
const [allProjectConfigs, setAllProjectConfigs] = useState<Array<{ projectId: string; config: INightWatchConfig }>>([]);
const [queueStatus, setQueueStatus] = useState<IQueueStatus | null>(null);
const [queueAnalytics, setQueueAnalytics] = useState<IQueueAnalytics | null>(null);
const [queueStatusError, setQueueStatusError] = useState<Error | null>(null);
const [queueAnalyticsError, setQueueAnalyticsError] = useState<Error | null>(null);

const [editState, setEditState] = useState<IQueueEditState>({
isDirty: false,
Expand Down Expand Up @@ -139,11 +142,21 @@ const Scheduling: React.FC = () => {
if (globalModeLoading) return;
const fetchDashboard = () => {
fetchQueueStatus()
.then(setQueueStatus)
.catch(() => { /* silently ignore */ });
.then((status) => {
setQueueStatus(status);
setQueueStatusError(null);
})
.catch((err) => {
setQueueStatusError(err instanceof Error ? err : new Error(String(err)));
});
fetchQueueAnalytics(24)
.then(setQueueAnalytics)
.catch(() => { /* silently ignore */ });
.then((analytics) => {
setQueueAnalytics(analytics);
setQueueAnalyticsError(null);
})
.catch((err) => {
setQueueAnalyticsError(err instanceof Error ? err : new Error(String(err)));
});
};
fetchDashboard();
const interval = setInterval(fetchDashboard, 30000);
Expand Down Expand Up @@ -460,7 +473,7 @@ const Scheduling: React.FC = () => {
return `${activeTemplate.label} - ${activeTemplate.hints[job]}`;
};

const tabs = [
const tabs = useMemo(() => [
{
id: 'overview',
label: 'Overview',
Expand Down Expand Up @@ -913,7 +926,38 @@ const Scheduling: React.FC = () => {
</div>
),
},
];
{
id: 'queue',
label: 'Queue',
content: (
<QueueTab
queueStatus={queueStatus}
queueAnalytics={queueAnalytics}
queueStatusError={queueStatusError}
queueAnalyticsError={queueAnalyticsError}
/>
),
},
], [
agents,
statusColor,
statusText,
isPaused,
scheduleInfo,
config,
allProjectConfigs,
queueStatus,
queueAnalytics,
queueStatusError,
queueAnalyticsError,
toggling,
saving,
editState,
showAddBucket,
newBucketKey,
newBucketConcurrency,
activeTemplate,
]);

return (
<div className="space-y-6 max-w-6xl mx-auto">
Expand Down
Loading
Loading